diff options
-rw-r--r-- | tagit/actions/__init__.py | 4 | ||||
-rw-r--r-- | tagit/actions/filter.py | 20 | ||||
-rw-r--r-- | tagit/apps/port-config.yaml | 8 | ||||
-rw-r--r-- | tagit/parsing/filter/__init__.py | 4 | ||||
-rw-r--r-- | tagit/parsing/filter/from_string.py | 20 | ||||
-rw-r--r-- | tagit/parsing/filter/to_string.py | 255 | ||||
-rw-r--r-- | tagit/utils/bsfs.py | 2 | ||||
-rw-r--r-- | tagit/widgets/filter.py | 39 | ||||
-rw-r--r-- | tagit/widgets/session.py | 1 | ||||
-rw-r--r-- | test/parsing/filter/test_to_string.py | 111 |
10 files changed, 424 insertions, 40 deletions
diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py index fa2bed0..b2ab6bd 100644 --- a/tagit/actions/__init__.py +++ b/tagit/actions/__init__.py @@ -64,8 +64,8 @@ class ActionBuilder(BuilderBase): 'GoBack': filter.GoBack, 'GoForth': filter.GoForth, 'JumpToToken': filter.JumpToToken, - #'SearchByAddressOnce': filter.SearchByAddressOnce, - #'SearchmodeSwitch': filter.SearchmodeSwitch, + 'SearchByAddressOnce': filter.SearchByAddressOnce, + 'SearchmodeSwitch': filter.SearchmodeSwitch, ## grouping 'CreateGroup': grouping.CreateGroup, 'DissolveGroup': grouping.DissolveGroup, diff --git a/tagit/actions/filter.py b/tagit/actions/filter.py index e878952..c5cc912 100644 --- a/tagit/actions/filter.py +++ b/tagit/actions/filter.py @@ -13,11 +13,10 @@ import kivy.properties as kp # tagit imports from tagit import config, dialogues -from tagit.utils import errors, Frame +from tagit.utils import errors, ns, Frame +from tagit.utils.bsfs import ast from tagit.widgets import Binding from tagit.widgets.filter import FilterAwareMixin -#from tagit.parsing.search import ast_to_string, ast # FIXME: mb/port -#from tagit.storage.base import ns # FIXME: mb/port # inner-module imports from .action import Action @@ -91,16 +90,13 @@ class AddToken(Action): def apply(self, token=None): if token is None: - #sugg = {node.label for node in self.root.session.storage.all(ns.tagit.storage.base.Tag)} - # FIXME: mb/port/bsfs - #sugg = set(self.root.session.storage.all(ns.bsfs.Tag).label()) - sugg = {'hello', 'world'} + sugg = self.root.session.storage.all(ns.bsfs.Tag).label(node=False) dlg = dialogues.TokenEdit(suggestions=sugg) dlg.bind(on_ok=lambda wx: self.add_from_string(wx.text)) dlg.open() elif isinstance(token, str): self.add_from_string(token) - elif isinstance(token, ast.ASTNode): + elif isinstance(token, ast.filter.FilterExpression): self.add_token([token]) def add_from_string(self, text): @@ -129,10 +125,8 @@ class EditToken(Action): text = kp.StringProperty('Edit token') def apply(self, token): - #sugg = {node.label for node in self.root.session.storage.all(ns.tagit.storage.base.Tag)} # FIXME: mb/port - sugg = {'hello', 'world'} # FIXME: mb/port - #text = ast_to_string(token) - text = 'hello world' + sugg = self.root.session.storage.all(ns.bsfs.Tag).label(node=False) + text = self.root.session.filter_to_string(token) dlg = dialogues.TokenEdit(text=text, suggestions=sugg) dlg.bind(on_ok=lambda obj: self.on_ok(token, obj)) dlg.open() @@ -140,7 +134,7 @@ class EditToken(Action): def on_ok(self, token, obj): with self.root.filter as filter: try: - tokens_from_text = self.root.session.filter_from_string(obj.text) # FIXME: mb/port + tokens_from_text = self.root.session.filter_from_string(obj.text) except errors.ParserError as e: dialogues.Error(text=f'Invalid token: {e}').open() return diff --git a/tagit/apps/port-config.yaml b/tagit/apps/port-config.yaml index 5e874ef..a9907b7 100644 --- a/tagit/apps/port-config.yaml +++ b/tagit/apps/port-config.yaml @@ -40,10 +40,18 @@ ui: - SelectNone - SelectMulti - SelectRange + - AddToken + - GoBack + - GoForth + - SearchByAddressOnce + - SearchmodeSwitch - AddTag - EditTag - OpenGroup #- RepresentGroup + - Search + - ShowSelected + - RemoveSelected - OpenExternal - ShowHelp browser: diff --git a/tagit/parsing/filter/__init__.py b/tagit/parsing/filter/__init__.py index 88b6256..defb332 100644 --- a/tagit/parsing/filter/__init__.py +++ b/tagit/parsing/filter/__init__.py @@ -6,12 +6,12 @@ Author: Matthias Baumgartner, 2022 """ # inner-module imports from .from_string import FromString -#from .to_string import ToString +from .to_string import ToString # exports __all__ = ( 'FromString', - #'ToString', + 'ToString', ) ## EOF ## diff --git a/tagit/parsing/filter/from_string.py b/tagit/parsing/filter/from_string.py index 5a38723..ed24f63 100644 --- a/tagit/parsing/filter/from_string.py +++ b/tagit/parsing/filter/from_string.py @@ -16,7 +16,7 @@ from pyparsing import CaselessKeyword, Combine, Group, Optional, Or, Word, delim # tagit imports from tagit.parsing.datefmt import parse_datetime from tagit.utils import bsfs, errors, ns, ttime -from tagit.utils.bsfs import ast +from tagit.utils.bsfs import ast, URI # constants SEARCH_DELIM = '/' @@ -36,6 +36,9 @@ class FromString(): _DATETIME_PREDICATES = None _QUERY = None + # current schema. + schema: bsfs.schema.Schema + def __init__(self, schema: bsfs.schema.Schema): self.schema = schema @@ -51,7 +54,6 @@ class FromString(): def build_parser(self): """ """ - # valid predicates per type, as supplied by tagit.library # FIXME: # * range / type constraints # * how to filter predicates @@ -74,11 +76,11 @@ class FromString(): self._abb2uri = {pred.uri.fragment: pred.uri for pred in predicates} # FIXME: tie-breaking for duplicates self._uri2abb = {uri: fragment for fragment, uri in self._abb2uri.items()} # all predicates - _PREDICATES = {self._uri2abb[pred.uri] for pred in predicates} + _PREDICATES = {self._uri2abb[pred.uri] for pred in predicates} | {'id', 'group'} # FIXME: properly document additions # numeric predicates - _PREDICATES_NUMERIC = {self._uri2abb[pred.uri] for pred in predicates if isinstance(pred.range, bsfs.schema.Literal) and pred.range <= self.schema.literal(ns.bsfs.Number)} # FIXME: type check might become unnecessary + _PREDICATES_NUMERIC = {self._uri2abb[pred.uri] for pred in predicates if pred.range <= self.schema.literal(ns.bsfs.Number)} # datetime predicates - self._DATETIME_PREDICATES = {pred.uri for pred in predicates if isinstance(pred.range, bsfs.schema.Literal) and pred.range <= self.schema.literal(ns.bsfs.Time)} # FIXME: type check might become unnecessary + self._DATETIME_PREDICATES = {pred.uri for pred in predicates if pred.range <= self.schema.literal(ns.bsfs.Time)} _PREDICATES_DATETIME = {self._uri2abb[pred] for pred in self._DATETIME_PREDICATES} @@ -203,6 +205,14 @@ class FromString(): raise errors.ParserError('Invalid operator ({})'.format(exp.op), exp) tokens.append(tok) + elif exp.getName() == 'categorical' and exp.predicate.lower() == 'id': + values = [URI(s.strip()) for s in exp.value] + tokens.append(ast.filter.IsIn(*values)) + + elif exp.getName() == 'categorical' and exp.predicate.lower() == 'group': + values = [URI(s.strip()) for s in exp.value] + tokens.append(ast.filter.Any(ns.bse.group, ast.filter.IsIn(*values))) + elif exp.getName() == 'categorical': pred = self._abb2uri[exp.predicate.lower()] approx = False diff --git a/tagit/parsing/filter/to_string.py b/tagit/parsing/filter/to_string.py new file mode 100644 index 0000000..0b1a3e1 --- /dev/null +++ b/tagit/parsing/filter/to_string.py @@ -0,0 +1,255 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# tagit imports +from tagit.utils.bsfs import ast, matcher, URI +from tagit.utils import errors, ns + +# exports +__all__ = ('ToString', ) + + +## code ## + +class ToString(): + + def __init__(self, schema): + self.matches = matcher.Filter() + + self.schema = schema + predicates = {pred for pred in self.schema.predicates() if pred.domain <= self.schema.node(ns.bsfs.Entity)} + # shortcuts + self._abb2uri = {pred.uri.fragment: pred.uri for pred in predicates} # FIXME: tie-breaking for duplicates + self._uri2abb = {uri: fragment for fragment, uri in self._abb2uri.items()} + + def __call__(self, query): + """ + """ + # FIXME: test query class type + if self.matches(query, ast.filter.And(matcher.Rest())): + return ' / '.join(self._parse(sub) for sub in query) + return self._parse(query) + + def _parse(self, query): + cases = ( + self._has, + self._entity, + self._group, + self._tag, + self._range, + self._categorical, + ) + for clbk in cases: + result = clbk(query) + if result is not None: + return result + + raise errors.BackendError() + + def _has(self, query): + # Has(<pred>) <-> has <pred> + # Not(Has(<pred>)) <-> has no <pred> + has = ast.filter.Has( + matcher.Partial(ast.filter.Predicate), + ast.filter.GreaterThan(1, strict=False)) + if self.matches(query, has): + # FIXME: guard against predicate mismatch + return f'has {self._uri2abb[query.predicate.predicate]}' + if self.matches(query, ast.filter.Not(has)): + # FIXME: guard against predicate mismatch + return f'has no {self._uri2abb[query.predicate.predicate]}' + return None + + def _categorical(self, query): + if not isinstance(query, ast.filter._Branch): + return None + + # shortcuts + expr = query.expr + pred = self._uri2abb.get(query.predicate.predicate, None) + if pred is None: + return None + + # positive constraints + if isinstance(query, ast.filter.Any): + # approximate positive constraint + # Any(<pred>, Includes(<values>, approx=True)) -> pred ~ ("...", ...) + if self.matches(expr, matcher.Partial(ast.filter.Substring)): + return f'{pred} ~ {expr.value}' + if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring)))): + values = '", "'.join(sub.value for sub in expr) + return f'{pred} ~ ("{values}")' + + # exact positive constraint + # ast.filter.Any(<pred>, ast.filter.Includes(<values>, approx=False)) -> pred = ("...", ...) + if self.matches(expr, matcher.Partial(ast.filter.Equals)): + return f'{pred} = {expr.value}' + if self.matches(query, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals)))): + values = '", "'.join(sub.value for sub in expr) + return f'{pred} = ("{values}")' + + # negative constraints + if isinstance(query, ast.filter.All): + # approximate negative constraint + # ast.filter.All(<pred>, ast.filter.Excludes(<values>, approx=True)) -> pred !~ ("...", ...) + if self.matches(query, ast.filter.Not(matcher.Partial(ast.filter.Substring))): + return f'{pred} !~ "{expr.value}"' + if self.matches(query, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring))))): + values = '", "'.join(sub.value for sub in expr) + return f'{pred} !~ ("{values}")' + + # exact negative constraint + # ast.filter.All(<pred>, ast.filter.Excludes(<values>, approx=False)) -> pred != ("...", ...) + if self.matches(query, ast.filter.Not(matcher.Partial(ast.filter.Equals))): + return f'{pred} != "{expr.value}"' + if self.matches(query, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals))))): + values = '", "'.join(sub.value for sub in expr) + return f'{pred} != ("{values}")' + + return None + + def _tag(self, query): + # positive constraint + # ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Includes(..., approx=?))) <-> "...", ...; ~ "...", ... + if self.matches(query, ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, matcher.Any()))): + expr = query.expr.expr + # approximate positive constraint + if self.matches(expr, matcher.Partial(ast.filter.Substring)): + return f'~ {expr.value}' + if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring)))): + values = '", "'.join(sub.value for sub in expr) + return f'~ "{values}"' + # exact positive constraint + if self.matches(expr, matcher.Partial(ast.filter.Equals)): + return f'{expr.value}' + if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals)))): + values = '", "'.join(sub.value for sub in expr) + return f'"{values}"' + + # negative constraint + # ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Excludes(..., approx=?))) <-> ! "...", ... ; !~ "...", ... + if self.matches(query, ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Not(matcher.Any())))): + expr = query.expr.expr.expr + # approximate negative constraint + if self.matches(expr, matcher.Partial(ast.filter.Substring)): + return f'!~ {expr.value}' + if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring)))): + values = '", "'.join(sub.value for sub in expr) + return f'!~ "{values}"' + # exact negative constraint + if self.matches(expr, matcher.Partial(ast.filter.Equals)): + return f'! {expr.value}' + if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals)))): + values = '", "'.join(sub.value for sub in expr) + return f'! "{values}"' + + return None + + def _range(self, query): + # FIXME: handle dates and times! + # FIXME: use default/configurable separators from from_string + if not isinstance(query, ast.filter.Any): + return None + + expr = query.expr + pred = self._uri2abb.get(query.predicate.predicate, None) + if pred is None: + return None + + if self.matches(expr, matcher.Partial(ast.filter.Equals)): + return f'{pred} = {expr.value}' + if self.matches(expr, matcher.Partial(ast.filter.GreaterThan, strict=True)): + return f'{pred} > {expr.threshold}' + if self.matches(expr, matcher.Partial(ast.filter.GreaterThan, strict=False)): + return f'{pred} >= {expr.threshold}' + if self.matches(expr, matcher.Partial(ast.filter.LessThan, strict=True)): + return f'{pred} < {expr.threshold}' + if self.matches(expr, matcher.Partial(ast.filter.LessThan, strict=False)): + return f'{pred} <= {expr.threshold}' + if self.matches(expr, ast.filter.And( + matcher.Partial(ast.filter.GreaterThan), + matcher.Partial(ast.filter.LessThan))): + lo, hi = list(expr) + if self.matches(lo, matcher.Partial(ast.filter.LessThan)): + lo, hi = hi, lo + b_open = '(' if lo.strict else '[' + b_close = ')' if hi.strict else ']' + return f'{pred} = {b_open}{lo.threshold} - {hi.threshold}{b_close}' + """ + ast.filter.Any(<pred>, ast.filter.Between(lo, hi, lo_strict, hi_strict)) + pred <? hi + pred >? hi + pred = [lo, hi] + pred = (lo, hi) + pred = [lo, hi) + pred = (lo, hi] + """ + return None + + def _entity(self, query): + # defaults + negated = False + guids = set() + + def get_guids(value): + if isinstance(value, URI): + return {value} + else: # elif isinstance(query.value, Nodes): + return set(value.guids) + + if self.matches(query, matcher.Partial(ast.filter.Is)): + guids = get_guids(query.value) + elif self.matches(query, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))): + guids = {guid for sub in query for guid in get_guids(sub.value) } + elif self.matches(query, ast.filter.Not(matcher.Partial(ast.filter.Is))): + negated = True + guids = get_guids(query.value) + elif self.matches(query, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))): + negated = True + guids = {guid for sub in query for guid in get_guids(sub.value) } + + if len(guids) == 0: + # no matches + return None + # some matches + cmp = 'not in' if negated else 'in' + values = '", "'.join(guids) + return f'id {cmp} "{values}"' + + def _group(self, query): + # ast.filter.Any(ns.bse.group, ast.filter.Is(...)) <-> group = ("...", ...) + if not self.matches(query, ast.filter.Any(ns.bse.group, matcher.Any())): + return None + + def get_guids(value): + if isinstance(value, URI): + return {value} + else: # elif isinstance(query.value, Nodes): + return set(value.guids) + + expr = query.expr + guids = set() + negated = False + + if self.matches(expr, matcher.Partial(ast.filter.Is)): + guids = get_guids(expr.value) + elif self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))): + guids = {guid for sub in expr for guid in get_guids(sub.value) } + elif self.matches(expr, ast.filter.Not(matcher.Partial(ast.filter.Is))): + negated = True + guids = get_guids(expr.value) + elif self.matches(expr, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))): + negated = True + guids = {guid for sub in expr for guid in get_guids(sub.value) } + + if len(guids) == 0: # no matches + return None + # some matches + cmp = 'not in' if negated else 'in' + values = '", "'.join(guids) + return f'group {cmp} "{values}"' + +## EOF ## diff --git a/tagit/utils/bsfs.py b/tagit/utils/bsfs.py index 3c2b826..ab8baa5 100644 --- a/tagit/utils/bsfs.py +++ b/tagit/utils/bsfs.py @@ -10,7 +10,7 @@ import typing # bsfs imports from bsfs import schema, Open from bsfs.namespace import Namespace -from bsfs.query import ast +from bsfs.query import ast, matcher from bsfs.utils import URI, uuid # exports diff --git a/tagit/widgets/filter.py b/tagit/widgets/filter.py index 8a7c1a2..15aefd6 100644 --- a/tagit/widgets/filter.py +++ b/tagit/widgets/filter.py @@ -22,8 +22,8 @@ import kivy.properties as kp # tagit imports from tagit import config -#from tagit.parsing.search import ast, ast_to_string # FIXME: mb/port -from tagit.utils import bsfs, errors +from tagit.utils import bsfs, errors, ns +from tagit.utils.bsfs import ast, matcher # inner-module imports from .session import ConfigAwareMixin @@ -120,20 +120,25 @@ class Filter(BoxLayout, ConfigAwareMixin): return query, sort def abbreviate(self, token): - return 'T' - # FIXME: mb/port/parsing - if token.predicate() == 'tag': - return ','.join(list(token.condition())) - elif token.predicate() == 'entity': - return 'R' if isinstance(token.condition(), ast.SetInclude) else 'E' - else: - return { - 'group' : 'G', - 'time' : 'T', - 'altitude' : 'Alt', - 'longitude' : 'Lon', - 'latitude' : 'Lat', - }.get(token.predicate(), token.predicate().title()) + matches = matcher.Filter() + if matches(token, ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, matcher.Any()))): + # tag token + return self.root.session.filter_to_string(token) + if matches(token, matcher.Partial(ast.filter.Is)) or \ + matches(token, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))): + # exclusive token + return 'E' + if matches(token, ast.filter.Not(matcher.Partial(ast.filter.Is))) or \ + matches(token, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))): + # reduce token + return 'R' + if matches(token, ast.filter.Any(ns.bse.group, matcher.Any())): + # group token + return 'G' + if matches(token, ast.filter.Any(matcher.Partial(ast.filter.Predicate), matcher.Any())): + # generic token + return token.predicate.predicate.get('fragment', '?').title() + return '?' def show_address_once(self): """Single-shot address mode without changing the search mode.""" @@ -272,7 +277,7 @@ class Addressbar(TextInput): def __init__(self, tokens, **kwargs): super(Addressbar, self).__init__(**kwargs) - self.text = ast_to_string(ast.AND(tokens)) + self.text = self.root.session.filter_to_string(bsfs.ast.filter.And(tokens)) self._last_text = self.text def on_text_validate(self): diff --git a/tagit/widgets/session.py b/tagit/widgets/session.py index e97a688..c233a15 100644 --- a/tagit/widgets/session.py +++ b/tagit/widgets/session.py @@ -40,6 +40,7 @@ class Session(Widget): self.log = log # derived members self.filter_from_string = parsing.filter.FromString(self.storage.schema) + self.filter_to_string = parsing.filter.ToString(self.storage.schema) #self.sort_from_string = parsing.Sort(self.storage.schema) # FIXME: mb/port/parsing def __enter__(self): diff --git a/test/parsing/filter/test_to_string.py b/test/parsing/filter/test_to_string.py new file mode 100644 index 0000000..6df7360 --- /dev/null +++ b/test/parsing/filter/test_to_string.py @@ -0,0 +1,111 @@ +""" + +Part of the tagit test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import contextlib +import io +import unittest + +# tagit imports +with contextlib.redirect_stderr(io.StringIO()): + from tagit.parsing.filter.from_string import FromString + from tagit.utils import bsfs, errors, ns + from tagit.utils.bsfs import ast + + # objects to test + from tagit.parsing.filter.to_string import ToString + + +## code ## + +class TestFromString(unittest.TestCase): + def setUp(self): + self.schema = bsfs.schema.from_string(''' + # common external prefixes + prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> + prefix xsd: <http://www.w3.org/2001/XMLSchema#> + + # common bsfs prefixes + prefix bsfs: <http://bsfs.ai/schema/> + prefix bse: <http://bsfs.ai/schema/Entity#> + + # nodes + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:Tag rdfs:subClassOf bsfs:Node . + + # literals + bsfs:Time rdfs:subClassOf bsfs:Literal . + xsd:string rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . + + # predicates + bse:mime rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:string ; + bsfs:unique "true"^^xsd:boolean . + + bse:iso rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + + bse:time rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Time; + bsfs:unique "true"^^xsd:boolean . + + bse:tag rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range bsfs:Tag ; + bsfs:unique "false"^^xsd:boolean . + + bse:rank rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Entity ; + rdfs:range xsd:integer ; + bsfs:unique "false"^^xsd:boolean . + + ''') + self.to_string = ToString(self.schema) + self.from_string = FromString(self.schema) + + def test_has(self): + self.assertEqual('has iso', self.to_string(self.from_string('has iso'))) + #from debug import debug + #debug(locals(), globals()) + + def test_categorical(self): + self.assertEqual('iso = 100.0', self.to_string(self.from_string('iso = 100'))) + + def test_tag(self): + self.assertEqual('foobar', self.to_string(self.from_string('foobar'))) + self.assertEqual('not foobar', self.to_string(self.from_string('not foobar'))) + self.assertEqual('~ foobar', self.to_string(self.from_string('~ foobar'))) + self.assertEqual('!~ foobar', self.to_string(self.from_string('!~ foobar'))) + self.assertIn(self.to_string(self.from_string('foo, bar')), ('"foo", "bar"', '"bar", "foo"')) + + def test_range(self): + self.assertEqual('iso < 100.0', self.to_string(self.from_string('iso < 100'))) + self.assertEqual('iso <= 100.0', self.to_string(self.from_string('iso <= 100'))) + self.assertEqual('iso > 100.0', self.to_string(self.from_string('iso > 100'))) + self.assertEqual('iso >= 100.0', self.to_string(self.from_string('iso >= 100'))) + self.assertEqual('iso = [10.0 - 100.0]', self.to_string(self.from_string('iso = [10-100]'))) + raise NotImplementedError() # FIXME: test with dates! + + def test_entity(self): + self.assertEqual('id in "http://example.com/entity#1234"', + self.to_string(self.from_string('id in "http://example.com/entity#1234"'))) + + def test_group(self): + self.assertEqual('group in "http://example.com/group#1234"', + self.to_string(self.from_string('group in "http://example.com/group#1234"'))) + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## |