aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--tagit/actions/__init__.py14
-rw-r--r--tagit/actions/filter.py33
-rw-r--r--tagit/apps/port-config.yaml4
-rw-r--r--tagit/assets/icons/scalable/filter/add.svg102
-rw-r--r--tagit/assets/icons/scalable/filter/address.svg124
-rw-r--r--tagit/assets/icons/scalable/filter/go_back.svg158
-rw-r--r--tagit/assets/icons/scalable/filter/go_forth.svg154
-rw-r--r--tagit/assets/icons/scalable/filter/shingles.svg217
-rw-r--r--tagit/dialogues/__init__.py14
-rw-r--r--tagit/dialogues/autoinput.py73
-rw-r--r--tagit/dialogues/dialogue.kv114
-rw-r--r--tagit/dialogues/dialogue.py108
-rw-r--r--tagit/dialogues/error.py45
-rw-r--r--tagit/dialogues/license.t33
-rw-r--r--tagit/dialogues/simple_input.kv30
-rw-r--r--tagit/dialogues/simple_input.py55
-rw-r--r--tagit/dialogues/stoken.py40
-rw-r--r--tagit/parsing/__init__.py8
-rw-r--r--tagit/parsing/filter.py (renamed from tagit/parsing/search.py)169
-rw-r--r--tagit/parsing/sort.py17
-rw-r--r--tagit/utils/__init__.py1
-rw-r--r--tagit/utils/bsfs.py10
-rw-r--r--tagit/utils/namespaces.py30
-rw-r--r--tagit/utils/shared.py12
-rw-r--r--tagit/widgets/filter.py13
-rw-r--r--tagit/widgets/session.py4
-rw-r--r--test/parsing/test_filter.py751
-rw-r--r--test/parsing/test_search.py707
29 files changed, 2185 insertions, 863 deletions
diff --git a/.gitignore b/.gitignore
index f9c6fcf..767d1af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,9 +32,9 @@ tagit/external/setproperty/setproperty.c
tagit/external/setproperty/setproperty.cpython*
# assets
-tagit/assets/icons/kivy/browser/
-tagit/assets/icons/kivy/misc/
-tagit/assets/icons/kivy/planes/
-
+tagit/assets/icons/kivy/browser*
+tagit/assets/icons/kivy/filter*
+tagit/assets/icons/kivy/misc*
+tagit/assets/icons/kivy/planes*
## EOF ##
diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py
index 034c4a1..c34cbe8 100644
--- a/tagit/actions/__init__.py
+++ b/tagit/actions/__init__.py
@@ -57,13 +57,13 @@ class ActionBuilder(BuilderBase):
'SelectInvert': browser.SelectInvert,
'Select': browser.Select,
## filter
- #'AddToken': filter.AddToken,
- #'RemoveToken': filter.RemoveToken,
- #'SetToken': filter.SetToken,
- #'EditToken': filter.EditToken,
- #'GoBack': filter.GoBack,
- #'GoForth': filter.GoForth,
- #'JumpToToken': filter.JumpToToken,
+ 'AddToken': filter.AddToken,
+ 'RemoveToken': filter.RemoveToken,
+ 'SetToken': filter.SetToken,
+ 'EditToken': filter.EditToken,
+ 'GoBack': filter.GoBack,
+ 'GoForth': filter.GoForth,
+ 'JumpToToken': filter.JumpToToken,
#'SearchByAddressOnce': filter.SearchByAddressOnce,
#'SearchmodeSwitch': filter.SearchmodeSwitch,
## grouping
diff --git a/tagit/actions/filter.py b/tagit/actions/filter.py
index 3702879..e878952 100644
--- a/tagit/actions/filter.py
+++ b/tagit/actions/filter.py
@@ -12,14 +12,12 @@ from kivy.lang import Builder
import kivy.properties as kp
# tagit imports
-from tagit import config
-from tagit import dialogues
-#from tagit.parsing import ParserError # FIXME: mb/port
-#from tagit.parsing.search import ast_from_string, ast_to_string, ast # FIXME: mb/port
-#from tagit.storage.base import ns # FIXME: mb/port
-from tagit.utils import Frame
-from tagit.widgets.bindings import Binding
+from tagit import config, dialogues
+from tagit.utils import errors, Frame
+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
@@ -54,7 +52,7 @@ class SetToken(Action):
with self.root.filter as filter:
try:
# parse filter into tokens
- tokens = list(ast_from_string(text))
+ tokens = list(self.root.session.filter_from_string(text))
# grab current frame
filter.f_head.append(self.root.browser.frame)
@@ -93,7 +91,10 @@ 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)}
+ #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'}
dlg = dialogues.TokenEdit(suggestions=sugg)
dlg.bind(on_ok=lambda wx: self.add_from_string(wx.text))
dlg.open()
@@ -104,8 +105,8 @@ class AddToken(Action):
def add_from_string(self, text):
try:
- self.add_token(ast_from_string(text))
- except ParserError as e:
+ self.add_token(self.root.session.filter_from_string(text))
+ except errors.ParserError as e:
dialogues.Error(text=f'syntax error: {e}').open()
def add_token(self, tokens):
@@ -128,8 +129,10 @@ 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)}
- text = ast_to_string(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'
dlg = dialogues.TokenEdit(text=text, suggestions=sugg)
dlg.bind(on_ok=lambda obj: self.on_ok(token, obj))
dlg.open()
@@ -137,8 +140,8 @@ class EditToken(Action):
def on_ok(self, token, obj):
with self.root.filter as filter:
try:
- tokens_from_text = ast_from_string(obj.text)
- except ParserError as e:
+ tokens_from_text = self.root.session.filter_from_string(obj.text) # FIXME: mb/port
+ 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 501eacd..d5c45e9 100644
--- a/tagit/apps/port-config.yaml
+++ b/tagit/apps/port-config.yaml
@@ -20,6 +20,7 @@ ui:
- ShowDashboard
#- AddTag
#- EditTag
+ - AddToken
#- CreateGroup
#- DissolveGroup
#- SelectAll
@@ -30,6 +31,9 @@ ui:
#- SelectSingle
#- SelectMulti
#- SelectRange
+ filter:
+ - AddToken
+ - EditToken
context:
app:
- ShowSettings
diff --git a/tagit/assets/icons/scalable/filter/add.svg b/tagit/assets/icons/scalable/filter/add.svg
new file mode 100644
index 0000000..a544053
--- /dev/null
+++ b/tagit/assets/icons/scalable/filter/add.svg
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100mm"
+ height="100mm"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="add.svg"
+ inkscape:export-filename="../../kivy/filter/add.png"
+ inkscape:export-xdpi="7.6199999"
+ inkscape:export-ydpi="7.6199999">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1.979899"
+ inkscape:cx="106.95662"
+ inkscape:cy="169.42756"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:snap-global="true"
+ inkscape:snap-bbox="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1920"
+ inkscape:window-height="1151"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:pagecheckerboard="true"
+ units="mm"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="true"
+ inkscape:object-paths="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-center="true"
+ inkscape:snap-text-baseline="true"
+ inkscape:snap-page="true"
+ inkscape:lockguides="false">
+ <sodipodi:guide
+ position="188.97638,188.97638"
+ orientation="0,1"
+ id="guide1099"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="188.97638,188.97638"
+ orientation="1,0"
+ id="guide1101"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-167.28122,-322.85977)">
+ <path
+ style="fill:none;stroke:#c8c8c8;stroke-width:29.81036186;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 182.82699,337.92736 123.49426,144.97182 v 203.0089 L 406.37348,630.06527 V 482.89684 L 529.86773,337.92736 H 182.82699"
+ id="path851"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
diff --git a/tagit/assets/icons/scalable/filter/address.svg b/tagit/assets/icons/scalable/filter/address.svg
new file mode 100644
index 0000000..de30faa
--- /dev/null
+++ b/tagit/assets/icons/scalable/filter/address.svg
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100mm"
+ height="100mm"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="address.svg"
+ inkscape:export-filename="../../kivy/filter/address.png"
+ inkscape:export-xdpi="7.6199999"
+ inkscape:export-ydpi="7.6199999">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.9899495"
+ inkscape:cx="8.0179354"
+ inkscape:cy="311.11216"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:snap-global="true"
+ inkscape:snap-bbox="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1920"
+ inkscape:window-height="1151"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:pagecheckerboard="true"
+ units="mm"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="true"
+ inkscape:object-paths="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-center="true"
+ inkscape:snap-text-baseline="true"
+ inkscape:snap-page="true"
+ inkscape:lockguides="false">
+ <sodipodi:guide
+ position="188.97638,188.97638"
+ orientation="0,1"
+ id="guide1099"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="188.97638,188.97638"
+ orientation="1,0"
+ id="guide1101"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-167.28122,-322.85977)">
+ <rect
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:17.85590553;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"
+ id="rect817"
+ width="360.09683"
+ height="220.61481"
+ x="176.20917"
+ y="401.52875" />
+ <path
+ style="fill:none;stroke:#c8c8c8;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 364.4772,427.97815 v 173.777"
+ id="path833"
+ inkscape:connector-curvature="0" />
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:52.13865662px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.30346632"
+ x="195.92052"
+ y="571.07178"
+ id="text837"><tspan
+ sodipodi:role="line"
+ id="tspan835"
+ x="195.92052"
+ y="571.07178"
+ style="font-size:173.79553223px;fill:#ffffff;fill-opacity:1;stroke-width:1.30346632"><tspan
+ style="fill:#e6e6e6;fill-opacity:1;stroke-width:1.30346632"
+ id="tspan843">fo</tspan><tspan
+ style="fill:#828282;fill-opacity:1;stroke-width:1.30346632"
+ id="tspan841">o</tspan></tspan></text>
+ </g>
+</svg>
diff --git a/tagit/assets/icons/scalable/filter/go_back.svg b/tagit/assets/icons/scalable/filter/go_back.svg
new file mode 100644
index 0000000..a972c87
--- /dev/null
+++ b/tagit/assets/icons/scalable/filter/go_back.svg
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100mm"
+ height="100mm"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="go_back.svg"
+ inkscape:export-filename="../../kivy/filter/go_back.png"
+ inkscape:export-xdpi="7.6199999"
+ inkscape:export-ydpi="7.6199999">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="265.05952"
+ inkscape:cy="147.00905"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:snap-global="true"
+ inkscape:snap-bbox="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1920"
+ inkscape:window-height="1031"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:pagecheckerboard="true"
+ units="mm"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="true"
+ inkscape:object-paths="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-center="true"
+ inkscape:snap-text-baseline="true"
+ inkscape:snap-page="true"
+ inkscape:lockguides="false">
+ <sodipodi:guide
+ orientation="0,1"
+ position="13.637059,643.40404"
+ id="guide3788"
+ inkscape:locked="false" />
+ <sodipodi:guide
+ position="188.97638,188.97638"
+ orientation="0,1"
+ id="guide1099"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="188.97638,188.97638"
+ orientation="1,0"
+ id="guide1101"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="233.588,370"
+ orientation="1,0"
+ id="guide1107"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="144.36496,311.42857"
+ orientation="1,0"
+ id="guide1109"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="-77.142857,144.36496"
+ orientation="0,1"
+ id="guide1111"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="5.000315,233.58779"
+ orientation="0,1"
+ id="guide1113"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-167.28122,-322.85977)">
+ <g
+ id="g883"
+ transform="matrix(-1,0,0,1,711.07945,-5.2302858e-6)">
+ <path
+ inkscape:transform-center-y="-2.0036887e-06"
+ inkscape:transform-center-x="-64.791235"
+ d="m 543.79823,511.83615 -107.19675,61.89008 -107.19675,61.89007 0,-123.78015 0,-123.78014 107.19676,61.89007 z"
+ inkscape:randomized="0"
+ inkscape:rounded="0"
+ inkscape:flatsided="false"
+ sodipodi:arg2="1.0471976"
+ sodipodi:arg1="0"
+ sodipodi:r2="71.4645"
+ sodipodi:r1="142.929"
+ sodipodi:cy="511.83615"
+ sodipodi:cx="400.86923"
+ sodipodi:sides="3"
+ id="path2995"
+ style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:1.56328595;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ sodipodi:type="star" />
+ <rect
+ y="462.98917"
+ x="165.84547"
+ height="97.693985"
+ width="231.62929"
+ id="rect4750"
+ style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.08440304;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4.16880612, 2.08440306;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ </g>
+ </g>
+</svg>
diff --git a/tagit/assets/icons/scalable/filter/go_forth.svg b/tagit/assets/icons/scalable/filter/go_forth.svg
new file mode 100644
index 0000000..f4a246d
--- /dev/null
+++ b/tagit/assets/icons/scalable/filter/go_forth.svg
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100mm"
+ height="100mm"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="go_forth.svg"
+ inkscape:export-filename="../../kivy/filter/go_forth.png"
+ inkscape:export-xdpi="7.6199999"
+ inkscape:export-ydpi="7.6199999">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1.4"
+ inkscape:cx="-59.38416"
+ inkscape:cy="86.716973"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:snap-global="true"
+ inkscape:snap-bbox="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1920"
+ inkscape:window-height="1031"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:pagecheckerboard="true"
+ units="mm"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="true"
+ inkscape:object-paths="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-center="true"
+ inkscape:snap-text-baseline="true"
+ inkscape:snap-page="true"
+ inkscape:lockguides="false">
+ <sodipodi:guide
+ orientation="0,1"
+ position="13.637059,643.40404"
+ id="guide3788"
+ inkscape:locked="false" />
+ <sodipodi:guide
+ position="188.97638,188.97638"
+ orientation="0,1"
+ id="guide1099"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="188.97638,188.97638"
+ orientation="1,0"
+ id="guide1101"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="233.588,370"
+ orientation="1,0"
+ id="guide1107"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="144.36496,311.42857"
+ orientation="1,0"
+ id="guide1109"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="-77.142857,144.36496"
+ orientation="0,1"
+ id="guide1111"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="5.000315,233.58779"
+ orientation="0,1"
+ id="guide1113"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-167.28122,-322.85977)">
+ <path
+ sodipodi:type="star"
+ style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:1.56328595;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path2995"
+ sodipodi:sides="3"
+ sodipodi:cx="400.86923"
+ sodipodi:cy="511.83615"
+ sodipodi:r1="142.929"
+ sodipodi:r2="71.4645"
+ sodipodi:arg1="0"
+ sodipodi:arg2="1.0471976"
+ inkscape:flatsided="false"
+ inkscape:rounded="0"
+ inkscape:randomized="0"
+ d="m 543.79823,511.83615 -107.19675,61.89008 -107.19675,61.89007 0,-123.78015 0,-123.78014 107.19676,61.89007 z"
+ inkscape:transform-center-x="-64.791235"
+ inkscape:transform-center-y="-2.0036887e-06" />
+ <rect
+ style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.07793283;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4.15586594, 2.07793297;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"
+ id="rect4750"
+ width="230.19354"
+ height="97.693985"
+ x="167.28122"
+ y="462.98917" />
+ </g>
+</svg>
diff --git a/tagit/assets/icons/scalable/filter/shingles.svg b/tagit/assets/icons/scalable/filter/shingles.svg
new file mode 100644
index 0000000..a07b6ea
--- /dev/null
+++ b/tagit/assets/icons/scalable/filter/shingles.svg
@@ -0,0 +1,217 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100mm"
+ height="100mm"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="shingles.svg"
+ inkscape:export-filename="../../kivy/filter/shingles.png"
+ inkscape:export-xdpi="7.6199999"
+ inkscape:export-ydpi="7.6199999">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.98994949"
+ inkscape:cx="61.463033"
+ inkscape:cy="211.38181"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:snap-global="true"
+ inkscape:snap-bbox="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1920"
+ inkscape:window-height="1151"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:pagecheckerboard="true"
+ units="mm"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="true"
+ inkscape:object-paths="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-center="true"
+ inkscape:snap-text-baseline="true"
+ inkscape:snap-page="true"
+ inkscape:lockguides="false">
+ <sodipodi:guide
+ position="188.97638,188.97638"
+ orientation="0,1"
+ id="guide1099"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ <sodipodi:guide
+ position="188.97638,188.97638"
+ orientation="1,0"
+ id="guide1101"
+ inkscape:locked="false"
+ inkscape:label=""
+ inkscape:color="rgb(0,0,255)" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-167.28122,-322.85977)">
+ <path
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"
+ d="m 762.61553,105.82518 v 80.22827 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-80.41973 z"
+ id="rect888-6-2-0-0"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <g
+ id="g1084"
+ transform="translate(139.39556,1.9249742)">
+ <g
+ transform="matrix(1.0298281,0,0,1.0298281,-320.57448,-232.49785)"
+ id="g1010-6" />
+ </g>
+ <g
+ id="g1077"
+ transform="translate(-135.36045,-275.77165)">
+ <g
+ transform="matrix(1.0298281,0,0,1.0298281,-106.16686,-28.823189)"
+ id="g1066" />
+ </g>
+ <g
+ id="g1077-9-8"
+ transform="translate(-235.90303,68.096044)">
+ <g
+ transform="matrix(1.0298281,0,0,1.0298281,-106.16686,-28.823189)"
+ id="g1066-2-9" />
+ </g>
+ <g
+ id="g1370"
+ transform="matrix(1.2737193,0,0,1.2737193,-97.514563,-83.896439)">
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3"
+ d="m 175.74036,319.44885 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-2"
+ d="m 296.44726,319.44885 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-9"
+ d="m 417.15416,319.44885 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.09517,-56.03559 l 0.527,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-3"
+ d="m 235.82505,415.6943 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-1"
+ d="m 356.53195,415.6943 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-94"
+ d="m 477.23885,415.6943 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09517,56.03559 33.1897,3.4e-4 60.0954,-25.0878 60.0952,-56.03559 l 0.527,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-7"
+ d="m 115.11815,415.6943 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-8"
+ d="m 175.20284,511.93975 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-4"
+ d="m 295.90974,511.93975 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-5"
+ d="m 416.35313,512.03548 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18966,3.4e-4 60.09539,-25.0878 60.09519,-56.03559 l 0.527,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-0"
+ d="m 114.58063,608.1852 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-36"
+ d="m 235.02402,608.28093 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90553,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09538,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-10"
+ d="m 355.73092,608.28093 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect888-6-2-3-6"
+ d="m 476.43782,608.28093 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.0952,56.03559 33.1897,3.4e-4 60.0954,-25.0878 60.0952,-56.03559 l 0.527,-40.20986"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ </g>
+ <path
+ style="fill:none;stroke:#c8c8c8;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 126.32932,322.99173 H 587.5694"
+ id="path1391"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
diff --git a/tagit/dialogues/__init__.py b/tagit/dialogues/__init__.py
index bee5bf4..d7792cb 100644
--- a/tagit/dialogues/__init__.py
+++ b/tagit/dialogues/__init__.py
@@ -18,11 +18,11 @@ import typing
# inner-module imports
##from .spash import Splash
-#from .autoinput import AutoTextInput
+from .autoinput import AutoTextInput
#from .console import Console
#from .dir_creator import DirCreator
#from .dir_picker import DirPicker
-#from .error import Error
+from .error import Error
#from .export import Export
#from .file_creator import FileCreator
#from .file_picker import FilePicker
@@ -32,8 +32,8 @@ import typing
#from .path_picker import PathPicker
#from .progress import Progress
#from .project import Project
-#from .simple_input import SimpleInput
-#from .stoken import TokenEdit
+from .simple_input import SimpleInput
+from .stoken import TokenEdit
#from .yesno import YesNo
# exports
@@ -41,7 +41,7 @@ __all__: typing.Sequence[str] = (
#'Console',
#'DirCreator',
#'DirPicker',
- #'Error',
+ 'Error',
#'Export',
#'FileCreator',
#'FilePicker',
@@ -51,8 +51,8 @@ __all__: typing.Sequence[str] = (
#'PathPicker',
#'Progress',
#'Project',
- #'SimpleInput',
- #'TokenEdit',
+ 'SimpleInput',
+ 'TokenEdit',
#'YesNo',
)
diff --git a/tagit/dialogues/autoinput.py b/tagit/dialogues/autoinput.py
new file mode 100644
index 0000000..a036ed4
--- /dev/null
+++ b/tagit/dialogues/autoinput.py
@@ -0,0 +1,73 @@
+"""This is a simple example of how to use suggestion text.
+
+In this example you setup a word_list at the begining. In this case
+'the the quick brown fox jumps over the lazy old dog'. This list along
+with any new word written word in the textinput is available as a
+suggestion when you are typing. You can press tab to auto complete the text.
+
+Based on & thanks to akshayaurora:
+ https://gist.github.com/akshayaurora/fa5a68980af585e355668e5adce5f98b
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Modifications authored by: Matthias Baumgartner, 2022
+"""
+# standard imports
+from bisect import bisect
+
+# kivy imports
+from kivy.uix.textinput import TextInput
+import kivy.properties as kp
+
+# exports
+__all__ = ('AutoTextInput', )
+
+
+## code ##
+
+class AutoTextInput(TextInput):
+
+ sep = kp.StringProperty(',')
+ suffix = kp.StringProperty(' ')
+ vocabulary = kp.ListProperty()
+
+ def on_suggestion_text(self, wx, value):
+ if not value:
+ return
+
+ super(AutoTextInput, self).on_suggestion_text(wx, value)
+
+ def keyboard_on_key_down(self, window, keycode, text, modifiers):
+ if self.suggestion_text and keycode[1] == 'tab': # complete suggestion_text
+ self.insert_text(self.suggestion_text + self.sep + self.suffix)
+ self.suggestion_text = ''
+ return True
+ return super(AutoTextInput, self).keyboard_on_key_down(window, keycode, text, modifiers)
+
+ def on_text(self, wx, value):
+ # include all current text from textinput into the word list
+ # the kind of behavior sublime text has
+
+ # what's on the current line
+ temp = value[:value.rfind(self.sep)].split(self.sep)
+ temp = [s.strip() for s in temp]
+ # combine with static vocabulary
+ wordlist = sorted(set(self.vocabulary + temp))
+
+ # get prefix
+ prefix = value[value.rfind(self.sep)+1:].strip()
+ if not prefix:
+ return
+
+ # binary search on (sorted) wordlist
+ pos = bisect(wordlist, prefix)
+
+ # check if matching string found
+ if pos == len(wordlist) or not wordlist[pos].startswith(prefix):
+ self.suggestion_text = ''
+ return
+
+ # fetch suffix from wordlist
+ self.suggestion_text = wordlist[pos][len(prefix):]
+
+## EOF ##
diff --git a/tagit/dialogues/dialogue.kv b/tagit/dialogues/dialogue.kv
new file mode 100644
index 0000000..e23f0db
--- /dev/null
+++ b/tagit/dialogues/dialogue.kv
@@ -0,0 +1,114 @@
+#:import get_root tagit.utils.get_root
+# FIXME: remove need for get_root
+
+<-Dialogue>:
+ auto_dismiss: True
+ ok_on_enter: True
+
+<DialogueContentBase>:
+
+ orientation: 'vertical'
+ padding: '12dp'
+ size_hint: 0.66, None
+ height: self.minimum_height
+
+ canvas:
+ # mask main window
+ Color:
+ rgba: 0,0,0, 0.7 * self.parent._anim_alpha
+ Rectangle:
+ size: self.parent._window.size if self.parent._window else (0, 0)
+
+ # solid background color
+ Color:
+ rgb: 1, 1, 1
+ BorderImage:
+ source: self.parent.background
+ border: self.parent.border
+ pos: self.pos
+ size: self.size
+
+
+<DialogueContentNoTitle@DialogueContentBase>:
+ # nothing to do
+
+<DialogueContentTitle@DialogueContentBase>:
+ title: ''
+ title_color: 1,1,1,1
+
+ Label:
+ text: root.title
+ size_hint_y: None
+ height: self.texture_size[1] + dp(16)
+ text_size: self.width - dp(16), None
+ font_size: '16sp'
+ color: root.title_color
+ bold: True
+ halign: 'center'
+ valing: 'middle'
+
+ canvas.before:
+ # Background
+ Color:
+ rgb: 0.2, 0.2, 0.2
+ Rectangle:
+ size: self.size
+ pos: self.pos
+
+ # top border
+ #Color:
+ # rgb: 0.5, 0.5, 0.5
+ #Line:
+ # points: self.x, self.y + self.height, self.x + self.width, self.y + self.height
+ # width: 2
+
+ # bottom border
+ #Color:
+ # rgb: 0.5, 0.5, 0.5
+ #Line:
+ # points: self.x, self.y, self.x + self.width, self.y
+ # width: 2
+
+ # small space
+ Label:
+ size_hint_y: None
+ height: 12
+
+
+<DialogueButtons>:
+ orientation: 'vertical'
+ size_hint_y: None
+ height: dp(48+8)
+
+ # small space
+ Label:
+ size_hint_y: None
+ height: dp(8)
+
+ # here come the buttons
+
+
+<DialogueButtons_One@DialogueButtons>:
+ ok_text: 'OK'
+
+ Button:
+ text: root.ok_text
+ on_press: get_root(self).ok()
+
+
+<DialogueButtons_Two@DialogueButtons>:
+ cancel_text: 'Cancel'
+ ok_text: 'OK'
+ ok_enabled: True
+
+ BoxLayout:
+ orientation: 'horizontal'
+ Button:
+ text: root.cancel_text
+ on_press: get_root(self).cancel()
+ Button:
+ text: root.ok_text
+ on_press: get_root(self).ok()
+ disabled: not root.ok_enabled
+
+## EOF ##
diff --git a/tagit/dialogues/dialogue.py b/tagit/dialogues/dialogue.py
new file mode 100644
index 0000000..1aa0e9a
--- /dev/null
+++ b/tagit/dialogues/dialogue.py
@@ -0,0 +1,108 @@
+"""Popup dialogue.
+
+Rougly based on code from https://gist.github.com/kived/742397a80d61e6be225a
+by Ryan Pessa. The license is provided in the source folder.
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Modifications authored by: Matthias Baumgartner, 2022
+"""
+# standard imports
+import os
+
+# kivy imports
+from kivy.lang import Builder
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.popup import Popup
+import kivy.properties as kp
+
+# exports
+__all__ = ('Dialogue', )
+
+
+## code ##
+
+# Load kv
+Builder.load_file(os.path.join(os.path.dirname(__file__), 'dialogue.kv'))
+
+# classes
+class Dialogue(Popup):
+ """Popup dialogue base class.
+
+ Use like below:
+
+ >>> dlg = Dialogue()
+ >>> dlg.bind(on_ok=....)
+ >>> dlg.open()
+
+ """
+
+ ok_on_enter = kp.BooleanProperty()
+
+ __events__ = ('on_ok', 'on_cancel')
+
+ def __init__(self, *args, **kwargs):
+ super(Dialogue, self).__init__(*args, **kwargs)
+ from kivy.core.window import Window
+ # assumes that the first widget created controls the keyboard
+ #Window.children[-1].request_exclusive_keyboard()
+ # Alternatively, you can bind a function (self._on_keyboard) to on_keyboard
+ # which returns True. This stops the event from being processed by the main
+ # window.
+ # However, this still does not keep the 'enter' from 'on_text_validate' from
+ # being processed by the main window.
+ Window.bind(on_keyboard=self._on_keyboard)
+ # By binding to on_key_down, the <enter> key can trigger the ok action.
+ # This also prevents the enter event to be processed by the main window,
+ # unlike the 'on_text_validate' of TextInput.
+ Window.bind(on_key_down=self._key_down)
+
+ def _on_keyboard(self, *args, **kwargs):
+ # block events from processing in the main window
+ return True
+
+ def _key_down(self, instance, key, scancode, codepoint, modifiers):
+ if key == 13 and self.ok_on_enter:
+ self.ok()
+ return True
+ # must not stop other events such that ctrl up/down reach the browser
+
+ def ok(self):
+ """User pressed the OK button."""
+ self.dispatch('on_ok')
+ self.dismiss()
+
+ def cancel(self):
+ """User pressed the Cancel button."""
+ self.dispatch('on_cancel')
+ self.dismiss()
+
+ def on_dismiss(self):
+ from kivy.core.window import Window
+ # assumes that the first widget created controls the keyboard
+ #Window.children[-1].release_exclusive_keyboard()
+ Window.unbind(on_keyboard=self._on_keyboard)
+ Window.unbind(on_key_down=self._key_down)
+ super(Dialogue, self).on_dismiss()
+
+ def on_ok(self):
+ """Event prototype."""
+ pass
+
+ def on_cancel(self):
+ """Event prototype."""
+ pass
+
+# helper classes
+
+# content bases
+class DialogueContentBase(BoxLayout): pass
+class DialogueContentTitle(DialogueContentBase): pass
+class DialogueContentNoTitle(DialogueContentBase): pass
+
+# buttons
+class DialogueButtons(BoxLayout): pass
+class DialogueButtons_One(DialogueButtons): pass
+class DialogueButtons_Two(DialogueButtons): pass
+
+## EOF ##
diff --git a/tagit/dialogues/error.py b/tagit/dialogues/error.py
new file mode 100644
index 0000000..d93f853
--- /dev/null
+++ b/tagit/dialogues/error.py
@@ -0,0 +1,45 @@
+"""Dialogue to show an error message.
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# kivy imports
+from kivy.lang import Builder
+import kivy.properties as kp
+
+# inner-module imports
+from .dialogue import Dialogue
+
+# exports
+__all__ = ('Error', )
+
+
+## code ##
+
+# load kv
+Builder.load_string('''
+<Error>:
+ text: ''
+ ok_on_enter: False
+
+ DialogueContentNoTitle:
+
+ Label:
+ markup: True
+ text: root.text
+ size_hint_y: None
+ color: 1, 0, 0, 1
+ height: self.texture_size[1] + dp(16)
+ text_size: self.width - dp(16), None
+ halign: 'center'
+
+ DialogueButtons_One:
+''')
+
+# classes
+class Error(Dialogue):
+ """Error message."""
+ text = kp.StringProperty('')
+
+## EOF ##
diff --git a/tagit/dialogues/license.t b/tagit/dialogues/license.t
new file mode 100644
index 0000000..bbd2830
--- /dev/null
+++ b/tagit/dialogues/license.t
@@ -0,0 +1,33 @@
+
+The dialogues are based on the following code:
+
+https://gist.github.com/kived/742397a80d61e6be225a
+
+It ships with license, provided below:
+
+>>> The following license shall apply to all Public Gists owned by account. It
+>>> shall never apply to any Secret Gists, for which no license of any sort is
+>>> granted.
+>>>
+>>> Copyright (c) 2015- Ryan Pessa
+>>>
+>>> Permission is hereby granted, free of charge, to any person obtaining a copy
+>>> of this software and associated documentation files (the "Software"), to deal
+>>> in the Software without restriction, including without limitation the rights
+>>> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+>>> copies of the Software, and to permit persons to whom the Software is
+>>> furnished to do so, subject to the following conditions:
+>>>
+>>> The above copyright notice and this permission notice shall be included in
+>>> all copies or substantial portions of the Software.
+>>>
+>>> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+>>> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+>>> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+>>> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+>>> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+>>> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+>>> THE SOFTWARE.
+
+Code modification are subject to the license of the tagit software.
+
diff --git a/tagit/dialogues/simple_input.kv b/tagit/dialogues/simple_input.kv
new file mode 100644
index 0000000..b7deb9c
--- /dev/null
+++ b/tagit/dialogues/simple_input.kv
@@ -0,0 +1,30 @@
+
+#:import AutoTextInput tagit.dialogues
+
+<SimpleInput>:
+ text: ''
+ ok_on_enter: True
+ cancel_on_defocus: True
+
+ DialogueContentNoTitle:
+
+ #AutoTextInput:
+ TextInput:
+ vocabulary: root.suggestions
+ sep: root.suggestion_sep
+ suffix: root.suggestion_suffix
+ focus: True
+ text: root.text
+ size_hint_y: None
+ multiline: False
+ height: self.minimum_height
+ text_size: self.width - dp(16), None
+ halign: 'center'
+
+ on_text: root.text = self.text
+ on_focus: root.on_text_focus(*args)
+ #on_text_validate: root.ok() # handled via the ok_on_enter mechanism
+
+ DialogueButtons_Two:
+
+## EOF ##
diff --git a/tagit/dialogues/simple_input.py b/tagit/dialogues/simple_input.py
new file mode 100644
index 0000000..d7cc69f
--- /dev/null
+++ b/tagit/dialogues/simple_input.py
@@ -0,0 +1,55 @@
+"""Dialogue with a single-line text input field.
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import os
+
+# kivy imports
+from kivy.lang import Builder
+import kivy.properties as kp
+
+# inner-module imports
+from .dialogue import Dialogue
+
+# exports
+__all__ = ('SimpleInput', )
+
+
+## code ##
+
+# load kv
+Builder.load_file(os.path.join(os.path.dirname(__file__), 'simple_input.kv'))
+
+# classes
+class SimpleInput(Dialogue):
+ """Dialogue with a single-line text input field.
+
+ Pass the default text as **text**.
+
+ >>> SimpleInput(text='Hello world').open()
+
+ In case of touch events, they need to be inhibited to change the focus.
+
+ >>> FocusBehavior.ignored_touch.append(touch)
+
+ """
+
+ # Defocus problem:
+ # Buttons defocus when on_press, but on_release is ok.
+ # Touch events must be blocked via FocusBehavior
+
+ text = kp.StringProperty('')
+ cancel_on_defocus = kp.BooleanProperty(True)
+ suggestions = kp.ListProperty()
+ suggestion_sep = kp.StringProperty(',')
+ suggestion_suffix = kp.StringProperty(' ')
+
+
+ def on_text_focus(self, instance, focus):
+ if not focus and self.cancel_on_defocus:
+ self.dismiss()
+
+## EOF ##
diff --git a/tagit/dialogues/stoken.py b/tagit/dialogues/stoken.py
new file mode 100644
index 0000000..6e5427a
--- /dev/null
+++ b/tagit/dialogues/stoken.py
@@ -0,0 +1,40 @@
+"""Search token editor
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+
+"""
+# kivy imports
+from kivy.lang import Builder
+import kivy.properties as kp
+
+# inner-module imports
+from .simple_input import SimpleInput
+
+# exports
+__all__ = ('TokenEdit', )
+
+
+## code ##
+
+# Load kv
+Builder.load_string('''
+#:import AutoTextInput tagit.dialogues
+
+<TokenEdit>:
+ text: ''
+ ok_on_enter: True
+ cancel_on_defocus: True
+''')
+
+# classes
+class TokenEdit(SimpleInput):
+ """Search token editor
+ """
+ # TODO: Currently this is no different than SimpleInput.
+ # It should be extend to specify the type and getting help
+ # with editing ranges and alternative selection.
+ pass
+
+## EOF ##
diff --git a/tagit/parsing/__init__.py b/tagit/parsing/__init__.py
index 1c431a4..0070bf9 100644
--- a/tagit/parsing/__init__.py
+++ b/tagit/parsing/__init__.py
@@ -6,14 +6,14 @@ Author: Matthias Baumgartner, 2022
"""
# inner-module imports
from .datefmt import parse_datetime
-from .search import ast_from_string
-from .sort import sort_from_string
+from .filter import Filter
+from .sort import Sort
# exports
__all__ = (
- 'ast_from_string',
+ 'Filter',
+ 'Sort',
'parse_datetime',
- 'sort_from_string',
)
## EOF ##
diff --git a/tagit/parsing/search.py b/tagit/parsing/filter.py
index 10d0e7c..ea8df51 100644
--- a/tagit/parsing/search.py
+++ b/tagit/parsing/filter.py
@@ -1,7 +1,7 @@
"""User-specified search query parsing.
>>> q = "has mime / tag in (november, october) / ! Apfel / time < 10.10.2004 / iso in (100, 200)"
->>> ast = ast_from_string(q)
+>>> ast = filter_from_string(q)
Part of the tagit module.
A copy of the license is provided with the project.
@@ -14,37 +14,29 @@ from datetime import datetime
from pyparsing import CaselessKeyword, Combine, Group, Optional, Or, Word, delimitedList, nums, oneOf, ParseException, Literal, QuotedString, alphanums, alphas8bit, punc8bit
# tagit imports
-from tagit.utils import errors, ttime
+from tagit.utils import bsfs, errors, ns, ttime
+from tagit.utils.bsfs import ast
# inner-module imports
-from . import datefmt
-
-# exports
-__all__ = (
- 'ast_from_string',
- )
+from .datefmt import parse_datetime
# constants
SEARCH_DELIM = '/'
VALUE_DELIM = ','
-DEFAULT_PREDICATE = 'tag'
+# exports
+__all__ = (
+ 'Filter',
+ )
-## code ##
-class SearchParser():
+## code ##
- # valid predicates per type
- _PREDICATES_CATEGORICAL = None
- _PREDICATES_CONTINUOUS = None
- _PREDICATES_DATETIME = None
+class Filter():
# parsers
- _CATEGORICAL = None
- _CONTINUOUS = None
- _EXISTENCE = None
+ _DATETIME_PREDICATES = None
_QUERY = None
- _TAG = None
def __init__(self, schema: bsfs.schema.Schema):
self.schema = schema
@@ -61,9 +53,6 @@ class SearchParser():
def build_parser(self):
"""
"""
- # The *predicate* argument is for compatibility with predicate listener.
- # It's not actually used here.
-
# valid predicates per type, as supplied by tagit.library
# FIXME:
# * range / type constraints
@@ -79,9 +68,21 @@ class SearchParser():
> Target: Entity (allow others?) -> rfds:domain
> Require: searchable as specified in backend AND user-searchable as specified in frontend
"""
- self._PREDICATES_CATEGORICAL = self.schema.predicates(searchable=True, range=self.schema.tm.categorical) # FIXME!
- self._PREDICATES_CONTINUOUS = self.schema.predicates(searchable=True, range=self.schema.tm.numerical) # FIXME!
- self._PREDICATES_DATETIME = self.schema.predicates(searchable=True, range=self.schema.tm.datetime) # FIXME!
+ # all relevant predicates
+ predicates = {pred for pred in self.schema.predicates() if pred.domain <= self.schema.node(ns.bsfs.Entity)}
+ # filter through accept/reject lists
+ ... # FIXME
+ # 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()}
+ # all predicates
+ _PREDICATES = {self._uri2abb[pred.uri] for pred in predicates}
+ # 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
+ # 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
+ _PREDICATES_DATETIME = {self._uri2abb[pred] for pred in self._DATETIME_PREDICATES}
+
# terminal symbols
number = Group(Optional(oneOf('- +')) \
@@ -93,11 +94,11 @@ class SearchParser():
# FIXME: Non-ascii characters
# predicates
- predicate = Or([CaselessKeyword(p) for p in self._PREDICATES_CATEGORICAL]).setResultsName(
+ predicate = Or([CaselessKeyword(p) for p in _PREDICATES]).setResultsName(
'predicate')
- date_predicate = Or([CaselessKeyword(p) for p in self._PREDICATES_DATETIME]).setResultsName(
+ date_predicate = Or([CaselessKeyword(p) for p in _PREDICATES_DATETIME]).setResultsName(
'predicate')
- num_predicate = Or([CaselessKeyword(p) for p in self._PREDICATES_CONTINUOUS]).setResultsName(
+ num_predicate = Or([CaselessKeyword(p) for p in _PREDICATES_NUMERIC]).setResultsName(
'predicate')
# existence
@@ -106,7 +107,7 @@ class SearchParser():
PREDICATE := [predicate]
"""
op = (CaselessKeyword('has') ^ CaselessKeyword('has no') ^ CaselessKeyword('has not')).setResultsName('op')
- self._EXISTENCE = Group(op + predicate).setResultsName('existence')
+ _EXISTENCE = Group(op + predicate).setResultsName('existence')
# continuous
@@ -127,7 +128,7 @@ class SearchParser():
bclose = oneOf(') ] [').setResultsName('bclose')
bopen = oneOf('( [ ]').setResultsName('bopen')
op = Or([':', '=', 'in']).setResultsName('op')
- datefmt = datefmt.parse_datetime.DATETIME
+ datefmt = parse_datetime.DATETIME
rngn = num_predicate + op + bopen + number('lo') + rsepn + number('hi') + bclose ^ \
num_predicate + op + bopen + rsepn + number('hi') + bclose ^ \
num_predicate + op + bopen + number('lo') + rsepn + bclose
@@ -143,7 +144,7 @@ class SearchParser():
datefmt('vleft') + cmp('cleft') + date_predicate ^ \
datefmt('vleft') + cmp('cleft') + date_predicate + cmp('cright') + datefmt('vright')
# combined
- self._CONTINUOUS = Group(
+ _CONTINUOUS = Group(
Group(eqn).setResultsName('eq') ^
Group(eqd).setResultsName('eq') ^
Group(rngn).setResultsName('range') ^ \
@@ -161,7 +162,7 @@ class SearchParser():
"""
op = (CaselessKeyword('in') ^ CaselessKeyword('not in') ^ ':' ^ '=' ^ '!=' ^ '~' ^ '!~').setResultsName('op')
value = delimitedList(words, delim=VALUE_DELIM).setResultsName('value')
- self._CATEGORICAL = Group(predicate + op + ('(' + value + ')' | value) ).setResultsName('categorical')
+ _CATEGORICAL = Group(predicate + op + ('(' + value + ')' | value) ).setResultsName('categorical')
# tag shortcuts
@@ -173,35 +174,17 @@ class SearchParser():
"""
op = oneOf('! ~ !~').setResultsName('op')
value = delimitedList(words, delim=VALUE_DELIM).setResultsName('value')
- self._TAG = Group(Optional(op) + '(' + value + ')' ^ Optional(op) + value).setResultsName('tag')
+ _TAG = Group(Optional(op) + '(' + value + ')' ^ Optional(op) + value).setResultsName('tag')
# overall query
"""
QUERY := QUERY / QUERY | EXPR
"""
- self._QUERY = delimitedList(self._EXISTENCE | self._CONTINUOUS | self._CATEGORICAL | self._TAG, delim=SEARCH_DELIM)
+ self._QUERY = delimitedList(_EXISTENCE | _CONTINUOUS | _CATEGORICAL | _TAG, delim=SEARCH_DELIM)
return self
- def __del__(self):
- if self._QUERY is not None: # remove listener
- try:
- self.predicates.ignore(self.build_parser)
- except ImportError:
- # The import fails if python is shutting down.
- # In that case, the ignore becomes unnecessary anyway.
- pass
-
def __call__(self, search):
- # FIXME: mb/port/parsing
- #if self._QUERY is None:
- # # parsers were not initialized yet
- # self.build_parser()
- # # attach listener to receive future updates
- # self.predicates.listen(self.build_parser)
- # # FIXME: Additional filters would be handy
- # #self.predicates.listen(self.build_parser, self.predicates.scope.library)
-
try:
parsed = self._QUERY.parseString(search, parseAll=True)
except ParseException as e:
@@ -211,61 +194,58 @@ class SearchParser():
tokens = []
for exp in parsed:
if exp.getName() == 'existence':
+ pred = self._abb2uri[exp.predicate.lower()]
if 'op' not in exp: # prevented by grammar
raise errors.ParserError('Missing operator', exp)
elif exp.op == 'has':
- cond = ast.Existence()
+ tok = ast.filter.Has(pred)
elif exp.op in ('has no', 'has not'):
- cond = ast.Inexistence()
+ tok = ast.filter.Not(ast.filter.Has(pred))
else: # prevented by grammar
raise errors.ParserError('Invalid operator ({})'.format(exp.op), exp)
-
- tokens.append(
- ast.Token(exp.predicate.lower(), cond))
+ tokens.append(tok)
elif exp.getName() == 'categorical':
+ pred = self._abb2uri[exp.predicate.lower()]
+ approx = False
values = [s.strip() for s in exp.value]
if 'op' not in exp: # prevented by grammar
raise errors.ParserError('Missing operator', exp)
- elif exp.op in (':', '=', 'in'):
- cond = ast.SetInclude(values)
- elif exp.op in ('!=', 'not in'):
- cond = ast.SetExclude(values)
- elif exp.op == '~':
- cond = ast.SetInclude(values, approximate=True)
- elif exp.op == '!~':
- cond = ast.SetExclude(values, approximate=True)
+ if exp.op in ('~' '!~'):
+ approx = True
+ if exp.op in (':', '=', '~', 'in'):
+ tok = ast.filter.Any(pred, ast.filter.Includes(*values, approx=approx))
+ elif exp.op in ('!=', '!~', 'not in'):
+ tok = ast.filter.All(pred, ast.filter.Excludes(*values, approx=approx))
else: # prevented by grammar
raise errors.ParserError('Invalid operator ({})'.format(exp.op), exp)
-
- tokens.append(
- ast.Token(exp.predicate.lower(), cond))
+ tokens.append(tok)
elif exp.getName() == 'tag':
values = [s.strip() for s in exp.value]
if 'op' not in exp:
- cond = ast.SetInclude(values)
+ outer = ast.filter.Any
+ cond = ast.filter.Includes(*values)
elif exp.op == '~':
- cond = ast.SetInclude(values, approximate=True)
+ outer = ast.filter.Any
+ cond = ast.filter.Includes(*values, approx=True)
elif exp.op == '!':
- cond = ast.SetExclude(values)
+ outer = ast.filter.All
+ cond = ast.filter.Excludes(*values)
elif exp.op == '!~':
- cond = ast.SetExclude(values, approximate=True)
+ outer = ast.filter.All
+ cond = ast.filter.Excludes(*values, approx=True)
else: # prevented by grammar
raise errors.ParserError('Invalid operator ({})'.format(exp.op), exp)
+ tokens.append(outer(ns.bse.tag, ast.filter.Any(ns.bst.label, cond)))
- tokens.append(
- ast.Token(DEFAULT_PREDICATE, cond))
-
- elif exp.getName() == 'continuous':
-
+ elif exp.getName() == 'continuous': # FIXME: simplify and adapt bsfs.query.ast.filter.Between accordingly!
lo, hi = None, None
lo_inc, hi_inc = False, False
predicate = None
-
if 'eq' in exp:
# equation style
- predicate = exp.eq.predicate.lower()
+ predicate = self._abb2uri[exp.eq.predicate.lower()]
if ('>' in exp.eq.cleft and '<' in exp.eq.cright) or \
('<' in exp.eq.cleft and '>' in exp.eq.cright) or \
@@ -294,7 +274,7 @@ class SearchParser():
lo_inc = hi_inc = True
elif 'range' in exp: # value in [lo:hi]
- predicate = exp.range.predicate.lower()
+ predicate = self._abb2uri[exp.range.predicate.lower()]
if 'lo' in exp.range:
lo = exp.range.lo
@@ -307,7 +287,7 @@ class SearchParser():
raise errors.ParserError('Expression is neither a range nor an equation', exp)
# interpret values
- if predicate in set([p.lower() for p in self._PREDICATES_DATETIME]):
+ if predicate in self._DATETIME_PREDICATES:
# turn into datetime
lo, lfmt = datefmt.guess_datetime(lo) if lo is not None else (None, None)
@@ -357,7 +337,8 @@ class SearchParser():
raise errors.ParserError('Lower bound must not exceed upper bound', (lo, hi))
tokens.append(
- ast.Token(predicate, ast.TimeRange(lo, hi, lo_inc, hi_inc)))
+ ast.filter.Any(predicate,
+ ast.filter.Between(lo, hi, not lo_inc, not hi_inc)))
else: # date specification
# Check consistency
@@ -368,7 +349,8 @@ class SearchParser():
raise errors.ParserError('Lower bound must not exceed upper bound', (lo, hi))
tokens.append(
- ast.Token(predicate, ast.Datetime(lo, hi, lo_inc, hi_inc)))
+ ast.filter.Any(predicate,
+ ast.filter.Between(lo, hi, not lo_inc, not hi_inc)))
else:
# number predicate
@@ -379,27 +361,14 @@ class SearchParser():
if not (lo < hi or (lo == hi and lo_inc and hi_inc)):
raise errors.ParserError('Lower bound must not exceed upper bound', (lo, hi))
+ # FIXME: mb/port: Three times the same code... optimize
tokens.append(
- ast.Token(predicate, ast.Continuous(lo, hi, lo_inc, hi_inc)))
+ ast.filter.Any(predicate,
+ ast.filter.Between(lo, hi, not lo_inc, not hi_inc)))
else: # prevented by grammar
raise errors.ParserError('Invalid expression', exp)
- return ast.AND(tokens)
-
-
-
-"""Default SearchParser instance.
-
-To produce an ast, call
-
->>> ast_from_string(search)
-
-Convenience shortcut for
-
->>> SearchParser().parse(search)
-
-"""
-ast_from_string = SearchParser(predicates)
+ return ast.filter.And(tokens)
## EOF ##
diff --git a/tagit/parsing/sort.py b/tagit/parsing/sort.py
index 8950613..75fa36c 100644
--- a/tagit/parsing/sort.py
+++ b/tagit/parsing/sort.py
@@ -12,13 +12,13 @@ from tagit.utils import errors, Struct
# exports
__all__ = (
- 'sort_from_string',
+ 'Sort',
)
## code ##
-class SortParser():
+class Sort():
"""Sort parser.
A sort string can be as simple as a predicate, but also allows
@@ -176,17 +176,4 @@ class SortParser():
else:
return ast.Order(*tokens)
-"""Default SortParser instance.
-
-To produce an ast, call
-
->>> sort_from_string(sort)
-
-Convenience shortcut for
-
->>> SortParser().parse(sort)
-
-"""
-sort_from_string = SortParser(sortkeys)
-
## EOF ##
diff --git a/tagit/utils/__init__.py b/tagit/utils/__init__.py
index 3f09078..16dcd4d 100644
--- a/tagit/utils/__init__.py
+++ b/tagit/utils/__init__.py
@@ -9,6 +9,7 @@ import typing
# inner-module imports
from . import bsfs
+from . import namespaces as ns
from . import time as ttime
from .frame import Frame
from .shared import * # FIXME: port properly
diff --git a/tagit/utils/bsfs.py b/tagit/utils/bsfs.py
index 0ab90a9..d80efe0 100644
--- a/tagit/utils/bsfs.py
+++ b/tagit/utils/bsfs.py
@@ -8,8 +8,16 @@ Author: Matthias Baumgartner, 2022
import typing
# bsfs imports
+from bsfs import schema, Open
+from bsfs.query import ast
+from bsfs.namespace import Namespace
# exports
-__all__: typing.Sequence[str] = []
+__all__: typing.Sequence[str] = (
+ 'Namespace',
+ 'Open',
+ 'ast',
+ 'schema',
+ )
## EOF ##
diff --git a/tagit/utils/namespaces.py b/tagit/utils/namespaces.py
new file mode 100644
index 0000000..dd26eef
--- /dev/null
+++ b/tagit/utils/namespaces.py
@@ -0,0 +1,30 @@
+"""Default namespaces used throughout tagit.
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import typing
+
+# inner-module imports
+from . import bsfs as _bsfs
+
+# constants
+bse = _bsfs.Namespace('http://bsfs.ai/schema/Entity')
+bsfs = _bsfs.Namespace('http://bsfs.ai/schema', fsep='/')
+bsm = _bsfs.Namespace('http://bsfs.ai/schema/Meta')
+bst = _bsfs.Namespace('http://bsfs.ai/schema/Tag')
+xsd = _bsfs.Namespace('http://www.w3.org/2001/XMLSchema')
+
+# export
+__all__: typing.Sequence[str] = (
+ 'bse',
+ 'bsfs',
+ 'bsm',
+ 'xsd',
+ )
+
+## EOF ##
+
+
diff --git a/tagit/utils/shared.py b/tagit/utils/shared.py
index 0d496ed..b5ab421 100644
--- a/tagit/utils/shared.py
+++ b/tagit/utils/shared.py
@@ -28,6 +28,7 @@ __all__: typing.Sequence[str] = (
'is_list',
'magnitude_fmt',
'truncate_dir',
+ 'get_root',
)
@@ -140,4 +141,15 @@ def fileopen(pth):
except KeyError:
warnings.warn('Unknown platform {}'.format(platform.system()))
+
+def get_root(obj):
+ """Traverse the widget tree upwards until the root is found."""
+ while obj.parent is not None and obj.parent != obj.parent.parent:
+ if hasattr(obj, 'root') and obj.root is not None:
+ return obj.root
+
+ obj = obj.parent
+
+ return obj
+
## EOF ##
diff --git a/tagit/widgets/filter.py b/tagit/widgets/filter.py
index 0152737..332ad34 100644
--- a/tagit/widgets/filter.py
+++ b/tagit/widgets/filter.py
@@ -23,7 +23,7 @@ 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 errors
+from tagit.utils import bsfs, errors
# inner-module imports
from .session import ConfigAwareMixin
@@ -108,6 +108,10 @@ class Filter(BoxLayout, ConfigAwareMixin):
## exposed methods
def get_query(self):
+ query = bsfs.ast.filter.And(self.t_head[:]) if len(self.t_head) > 0 else None
+ sort = None
+ return query, sort
+ # FIXME: mb/port.parsing
query = ast.AND(self.t_head[:]) if len(self.t_head) else None
# sort order is always set to False so that changing the sort order
# won't trigger a new query which can be very expensive. The sort
@@ -116,6 +120,8 @@ 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':
@@ -162,8 +168,9 @@ class Filter(BoxLayout, ConfigAwareMixin):
if self.changed:
self.redraw()
# issue search
- if self.run_search:
- self.root.trigger('Search')
+ # FIXME: mb/port/parsing
+ #if self.run_search:
+ # self.root.trigger('Search')
def redraw(self):
self.tokens.clear_widgets()
diff --git a/tagit/widgets/session.py b/tagit/widgets/session.py
index a7c7355..ca8c595 100644
--- a/tagit/widgets/session.py
+++ b/tagit/widgets/session.py
@@ -14,6 +14,7 @@ from kivy.uix.widget import Widget
import kivy.properties as kp
# tagit imports
+from tagit import parsing
from tagit.config.loader import load_settings
#from tagit.storage.broker import Broker # FIXME: mb/port
#from tagit.storage.loader import load_broker, load_log # FIXME: mb/port
@@ -38,6 +39,9 @@ class Session(Widget):
self.cfg = cfg
self.storage = storage
self.log = log
+ # derived members
+ self.filter_from_string = parsing.Filter(self.storage.schema)
+ #self.sort_from_string = parsing.Sort(self.storage.schema) # FIXME: mb/port/parsing
def __enter__(self):
return self
diff --git a/test/parsing/test_filter.py b/test/parsing/test_filter.py
new file mode 100644
index 0000000..c01c1bf
--- /dev/null
+++ b/test/parsing/test_filter.py
@@ -0,0 +1,751 @@
+"""
+
+Part of the tagit test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import unittest
+from datetime import datetime
+
+# external imports
+from pyparsing import ParseException
+
+# tagit imports
+from tagit.utils import bsfs, errors, ns
+from tagit.utils.bsfs import ast
+
+# objects to test
+from tagit.parsing.filter import Filter
+
+
+## code ##
+
+class TestFilterRange(unittest.TestCase):
+ longMessage = True
+
+ def setUp(self):
+ #predicates.expose('mime', TestScope('attribute', 'mime'), 'Categorical')
+ #predicates.expose('iso', TestScope('attribute', 'iso'), 'Continuous', 'Categorical')
+ #predicates.expose('time', TestScope('generic', 't_image_create_loc'), 'TimeRange', 'Datetime')
+ #predicates.expose('tag', TestScope('generic', 'tag'), 'Categorical')
+
+ #predicates.expose('mime', TestScope('attribute', 'mime'), 'Categorical')
+ #predicates.expose('rank', TestScope('attribute', 'rank'), 'Continuous')
+ #predicates.expose('iso', TestScope('attribute', 'iso'), 'Continuous', 'Categorical')
+ #predicates.expose('time', TestScope('generic', 't_image_create_loc'), 'TimeRange', 'Datetime')
+ #predicates.expose('tag', TestScope('generic', 'tag'), 'Categorical')
+
+
+ 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.parse = Filter(self.schema)
+
+ def _test_number(self, query, target):
+ predicate, condition = target
+ result = self.parse(query)
+ target = ast.filter.And(ast.filter.Any(predicate, condition))
+ self.assertEqual(result, target, msg="in query '{}'".format(query))
+
+ def test_larger_than(self):
+ # larger than A (inclusive)
+ for editable in [
+ # range
+ "{predicate} in [{num}:]", "{predicate} in [{num}:[", "{predicate} in [{num}:)",
+ "{predicate} : [{num}:]", "{predicate} : [{num}:[", "{predicate} : [{num}:)",
+ "{predicate} = [{num}:]", "{predicate} = [{num}:[", "{predicate} = [{num}:)",
+ ]:
+ # positive
+ self._test_number(editable.format(num=1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.GreaterThan(1.23, False)))
+ # negative
+ self._test_number(editable.format(num=-1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.GreaterThan(-1.23, False)))
+
+ for editable in [
+ # range
+ "{predicate} in [{num}-]", "{predicate} in [{num}-[", "{predicate} in [{num}-)",
+ "{predicate} : [{num}-]", "{predicate} : [{num}-[", "{predicate} : [{num}-)",
+ "{predicate} = [{num}-]", "{predicate} = [{num}-[", "{predicate} = [{num}-)",
+ # equation
+ "{predicate} >= {num}", "{num} <= {predicate}",
+ ]:
+ # positive
+ self._test_number(editable.format(num=1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.GreaterThan(1.23, False)))
+ # negative
+ self._test_number(editable.format(num=-1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.GreaterThan(-1.23, False)))
+ # FIXME: date
+ #self._test_number(editable.format(predicate='time', num="30.04.2012, 13:18"),
+ # ('time', ast.Datetime(datetime(2012, 4, 30, 13, 18), datetime.max, True, False)))
+
+ # larger than A (exclusive)
+ for editable in [
+ # range / bracket
+ "{predicate} in ]{num}:]", "{predicate} in ]{num}:[", "{predicate} in ]{num}:)",
+ "{predicate} : ]{num}:]", "{predicate} : ]{num}:[", "{predicate} : ]{num}:)",
+ "{predicate} = ]{num}:]", "{predicate} = ]{num}:[", "{predicate} = ]{num}:)",
+ # range / parenthesis
+ "{predicate} in ({num}:]", "{predicate} in ({num}:[", "{predicate} in ({num}:)",
+ "{predicate} : ({num}:]", "{predicate} : ({num}:[", "{predicate} : ({num}:)",
+ "{predicate} = ({num}:]", "{predicate} = ({num}:[", "{predicate} = ({num}:)",
+ ]:
+ # positive
+ self._test_number(editable.format(num=1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.GreaterThan(1.23, True)))
+ # negative
+ self._test_number(editable.format(num=-1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.GreaterThan(-1.23, True)))
+
+ for editable in [
+ # range / bracket
+ "{predicate} in ]{num}-]", "{predicate} in ]{num}-[", "{predicate} in ]{num}-)",
+ "{predicate} : ]{num}-]", "{predicate} : ]{num}-[", "{predicate} : ]{num}-)",
+ "{predicate} = ]{num}-]", "{predicate} = ]{num}-[", "{predicate} = ]{num}-)",
+ # range / parenthesis
+ "{predicate} in ({num}-]", "{predicate} in ({num}-[", "{predicate} in ({num}-)",
+ "{predicate} : ({num}-]", "{predicate} : ({num}-[", "{predicate} : ({num}-)",
+ "{predicate} = ({num}-]", "{predicate} = ({num}-[", "{predicate} = ({num}-)",
+ # equation
+ "{predicate} > {num}", "{num} < {predicate}",
+ ]:
+ # positive
+ self._test_number(editable.format(num=1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.GreaterThan(1.23, True)))
+ # negative
+ self._test_number(editable.format(num=-1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.GreaterThan(-1.23, True)))
+ # FIXME: date
+ #self._test_number(editable.format(predicate='time', num="30.04.2012, 13:18"),
+ # ('time', ast.Datetime(datetime(2012, 4, 30, 13, 19), datetime.max, True, False)))
+
+ def test_smaller_than(self):
+ # smaller than B (inclusive)
+ for editable in [
+ # range
+ "{predicate} in [:{num}]", "{predicate} in (:{num}]", "{predicate} in ]:{num}]",
+ "{predicate} : [:{num}]", "{predicate} : (:{num}]", "{predicate} : ]:{num}]",
+ "{predicate} = [:{num}]", "{predicate} = (:{num}]", "{predicate} = ]:{num}]",
+ ]:
+ # positives
+ self._test_number(editable.format(num=1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.LessThan(1.23, False)))
+ # negatives
+ self._test_number(editable.format(num=-1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.LessThan(-1.23, False)))
+
+ for editable in [
+ # range
+ "{predicate} in [-{num}]", "{predicate} in (-{num}]", "{predicate} in ]-{num}]",
+ "{predicate} : [-{num}]", "{predicate} : (-{num}]", "{predicate} : ]-{num}]",
+ "{predicate} = [-{num}]", "{predicate} = (-{num}]", "{predicate} = ]-{num}]",
+ # equation
+ "{predicate} <={num}", "{num} >= {predicate}",
+ ]:
+ # positives
+ self._test_number(editable.format(num=1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.LessThan(1.23, False)))
+ # negatives
+ self._test_number(editable.format(num=-1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.LessThan(-1.23, False)))
+ # FIXME: date
+ #self._test_number(editable.format(predicate='time', num="30.04.2012, 13:18"),
+ # ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 13, 19), False, False)))
+
+ # smaller than B (exclusive)
+ for editable in [
+ # range / bracket
+ "{predicate} in [:{num}[", "{predicate} in (:{num}[", "{predicate} in ]:{num}[",
+ "{predicate} : [:{num}[", "{predicate} : (:{num}[", "{predicate} : ]:{num}[",
+ "{predicate} = [:{num}[", "{predicate} = (:{num}[", "{predicate} = ]:{num}[",
+ # range / parenthesis
+ "{predicate} in [:{num})", "{predicate} in (:{num})", "{predicate} in ]:{num})",
+ "{predicate} : [:{num})", "{predicate} : (:{num})", "{predicate} : ]:{num})",
+ "{predicate} = [:{num})", "{predicate} = (:{num})", "{predicate} = ]:{num})",
+ ]:
+ # positives
+ self._test_number(editable.format(num=1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.LessThan(1.23, True)))
+ # negatives
+ self._test_number(editable.format(num=-1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.LessThan(-1.23, True)))
+
+ for editable in [
+ # range / bracket
+ "{predicate} in [-{num}[", "{predicate} in (-{num}[", "{predicate} in ]-{num}[",
+ "{predicate} : [-{num}[", "{predicate} : (-{num}[", "{predicate} : ]-{num}[",
+ "{predicate} = [-{num}[", "{predicate} = (-{num}[", "{predicate} = ]-{num}[",
+ # range / parenthesis
+ "{predicate} in [-{num})", "{predicate} in (-{num})", "{predicate} in ]-{num})",
+ "{predicate} : [-{num})", "{predicate} : (-{num})", "{predicate} : ]-{num})",
+ "{predicate} = [-{num})", "{predicate} = (-{num})", "{predicate} = ]-{num})",
+ # equation
+ "{predicate} <{num}", "{num} > {predicate}",
+ ]:
+ # positives
+ self._test_number(editable.format(num=1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.LessThan(1.23, True)))
+ # negatives
+ self._test_number(editable.format(num=-1.23, predicate='iso'),
+ (ns.bse.iso, ast.filter.LessThan(-1.23, True)))
+ # FIXME: date
+ #self._test_number(editable.format(predicate='time', num="30.04.2012, 13:18"),
+ # ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 13, 18), False, False)))
+
+ def test_between(self):
+ # between A and B (including A, including B)
+ for editable in [
+ # range
+ "{predicate} in [{numA}:{numB}]", "{predicate} : [{numA}:{numB}]", "{predicate} = [{numA}:{numB}]",
+ ]:
+ # positives
+ self._test_number(editable.format(predicate='iso', numA=1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(1.23, 4.56, False, False)))
+ # negatives
+ self._test_number(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
+ (ns.bse.iso, ast.filter.Between(-4.56, -1.23, False, False)))
+ # mixed
+ self._test_number(editable.format(predicate='iso', numA=-1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(-1.23, 4.56, False, False)))
+
+ for editable in [
+ # range
+ "{predicate} in [{numA}-{numB}]", "{predicate} : [{numA}-{numB}]", "{predicate} = [{numA}-{numB}]",
+ # equation
+ "{numA} <= {predicate} <= {numB}"
+ ]:
+ # positives
+ self._test_number(editable.format(predicate='iso', numA=1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(1.23, 4.56, False, False)))
+ # negatives
+ self._test_number(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
+ (ns.bse.iso, ast.filter.Between(-4.56, -1.23, False, False)))
+ # mixed
+ self._test_number(editable.format(predicate='iso', numA=-1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(-1.23, 4.56, False, False)))
+ # FIXME: date
+ #self._test_number(editable.format(predicate='time', numA="30.04.2012, 13:18", numB="13.6.2014, 18:27"),
+ # ('time', ast.Datetime(datetime(2012, 4, 30, 13, 18), datetime(2014, 6, 13, 18, 28), True, False)))
+
+ # between A and B (including A, excluding B)
+ for editable in [
+ # range
+ "{predicate} in [{numA}:{numB})", "{predicate} in [{numA}:{numB}[",
+ "{predicate} : [{numA}:{numB})", "{predicate} : [{numA}:{numB}[",
+ "{predicate} = [{numA}:{numB})", "{predicate} = [{numA}:{numB}[",
+ ]:
+ # positives
+ self._test_number(editable.format(predicate='iso', numA=1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(1.23, 4.56, False, True)))
+ # negatives
+ self._test_number(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
+ (ns.bse.iso, ast.filter.Between(-4.56, -1.23, False, True)))
+ # mixed
+ self._test_number(editable.format(predicate='iso', numA=-1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(-1.23, 4.56, False, True)))
+
+ for editable in [
+ # range
+ "{predicate} in [{numA}-{numB})", "{predicate} in [{numA}-{numB}[",
+ "{predicate} : [{numA}-{numB})", "{predicate} : [{numA}-{numB}[",
+ "{predicate} = [{numA}-{numB})", "{predicate} = [{numA}-{numB}[",
+ # equation
+ "{numA} <= {predicate} < {numB}",
+ ]:
+ # positives
+ self._test_number(editable.format(predicate='iso', numA=1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(1.23, 4.56, False, True)))
+ # negatives
+ self._test_number(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
+ (ns.bse.iso, ast.filter.Between(-4.56, -1.23, False, True)))
+ # mixed
+ self._test_number(editable.format(predicate='iso', numA=-1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(-1.23, 4.56, False, True)))
+ # FIXME: date
+ #self._test_number(editable.format(predicate='time', numA="30.04.2012, 13:18", numB="13.6.2014, 18:27"),
+ # ('time', ast.Datetime(datetime(2012, 4, 30, 13, 18), datetime(2014, 6, 13, 18, 27), True, False)))
+
+ # between A and B (excluding A, including B)
+ for editable in [
+ # range
+ "{predicate} in ({numA}:{numB}]", "{predicate} in ]{numA}:{numB}]",
+ "{predicate} : ({numA}:{numB}]", "{predicate} : ]{numA}:{numB}]",
+ "{predicate} = ({numA}:{numB}]", "{predicate} = ]{numA}:{numB}]",
+ ]:
+ # positives
+ self._test_number(editable.format(predicate='iso', numA=1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(1.23, 4.56, True, False)))
+ # negatives
+ self._test_number(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
+ (ns.bse.iso, ast.filter.Between(-4.56, -1.23, True, False)))
+ # mixed
+ self._test_number(editable.format(predicate='iso', numA=-1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(-1.23, 4.56, True, False)))
+
+ for editable in [
+ # range
+ "{predicate} in ({numA}-{numB}]", "{predicate} in ]{numA}-{numB}]",
+ "{predicate} : ({numA}-{numB}]", "{predicate} : ]{numA}-{numB}]",
+ "{predicate} = ({numA}-{numB}]", "{predicate} = ]{numA}-{numB}]",
+ # equation
+ "{numA} < {predicate} <= {numB}",
+ ]:
+ # positives
+ self._test_number(editable.format(predicate='iso', numA=1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(1.23, 4.56, True, False)))
+ # negatives
+ self._test_number(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
+ (ns.bse.iso, ast.filter.Between(-4.56, -1.23, True, False)))
+ # mixed
+ self._test_number(editable.format(predicate='iso', numA=-1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(-1.23, 4.56, True, False)))
+ # FIXME: date
+ #self._test_number(editable.format(predicate='time', numA="30.04.2012, 13:18", numB="13.6.2014, 18:27"),
+ # ('time', ast.Datetime(datetime(2012, 4, 30, 13, 19), datetime(2014, 6, 13, 18, 28), True, False)))
+
+ # between A and B (excluding A, excluding B)
+ for editable in [
+ "{predicate} in ({numA}:{numB})", "{predicate} in ]{numA}:{numB}[",
+ "{predicate} : ({numA}:{numB})", "{predicate} : ]{numA}:{numB}[",
+ "{predicate} = ({numA}:{numB})", "{predicate} = ]{numA}:{numB}[",
+ ]:
+ # positives
+ self._test_number(editable.format(predicate='iso', numA=1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(1.23, 4.56, True, True)))
+ # negatives
+ self._test_number(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
+ (ns.bse.iso, ast.filter.Between(-4.56, -1.23, True, True)))
+ # mixed
+ self._test_number(editable.format(predicate='iso', numA=-1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(-1.23, 4.56, True, True)))
+
+ for editable in [
+ "{predicate} in ({numA}-{numB})", "{predicate} in ]{numA}-{numB}[",
+ "{predicate} : ({numA}-{numB})", "{predicate} : ]{numA}-{numB}[",
+ "{predicate} = ({numA}-{numB})", "{predicate} = ]{numA}-{numB}[",
+ # equation
+ "{numA} < {predicate} < {numB}",
+ ]:
+ # positives
+ self._test_number(editable.format(predicate='iso', numA=1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(1.23, 4.56, True, True)))
+ # negatives
+ self._test_number(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
+ (ns.bse.iso, ast.filter.Between(-4.56, -1.23, True, True)))
+ # mixed
+ self._test_number(editable.format(predicate='iso', numA=-1.23, numB=4.56),
+ (ns.bse.iso, ast.filter.Between(-1.23, 4.56, True, True)))
+ # FIXME: date
+ #self._test_number(editable.format(predicate='time', numA="30.04.2012, 13:18", numB="13.6.2014, 18:27"),
+ # ('time', ast.Datetime(datetime(2012, 4, 30, 13, 19), datetime(2014, 6, 13, 18, 27), True, False)))
+
+ def test_equal(self):
+ # equal to A
+ for editable in [
+ # range
+ "{predicate} in [{num}:{num}]", "{predicate} : [{num}:{num}]", "{predicate} = [{num}:{num}]",
+ ]:
+ # positives
+ self._test_number(editable.format(predicate='iso', num=1.23),
+ (ns.bse.iso, ast.filter.Equals(1.23)))
+ # negatives
+ self._test_number(editable.format(predicate='iso', num=-1.23),
+ (ns.bse.iso, ast.filter.Equals(-1.23)))
+
+ for editable in [
+ # range
+ "{predicate} in [{num}-{num}]", "{predicate} : [{num}-{num}]", "{predicate} = [{num}-{num}]",
+ # equation
+ "{predicate} = {num}", "{num} = {predicate}",
+ ]:
+ # positives
+ self._test_number(editable.format(predicate='iso', num=1.23),
+ (ns.bse.iso, ast.filter.Equals(1.23)))
+ # negatives
+ self._test_number(editable.format(predicate='iso', num=-1.23),
+ (ns.bse.iso, ast.filter.Equals(-1.23)))
+ # FIXME: date
+ #self._test_number(editable.format(predicate='time', num="30.04.2012, 13:18"),
+ # ('time', ast.Datetime(datetime(2012, 4, 30, 13, 18), datetime(2012, 4, 30, 13, 19), True, False)))
+
+ def test_dates(self):
+ raise NotImplementedError() # FIXME
+ self._test_number("{predicate} < {num}".format(predicate='time', num="2012"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 1, 1), False, False)))
+ self._test_number("{predicate} < {num}".format(predicate='time', num="2012.04"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 4, 1), False, False)))
+ self._test_number("{predicate} < {num}".format(predicate='time', num="2012.04.30"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30), False, False)))
+ self._test_number("{predicate} < {num}".format(predicate='time', num="2012.04.30, 3 pm"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15), False, False)))
+ self._test_number("{predicate} < {num}".format(predicate='time', num="2012.04.30, 15:34"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 34), False, False)))
+ self._test_number("{predicate} < {num}".format(predicate='time', num="2012.04.30, 15:34:12"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 34, 12), False, False)))
+ self._test_number("{predicate} < {num}".format(predicate='time', num="2012.04.30, 15:34:12.98"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 34, 12, 980000), False, False)))
+
+ self._test_number("{predicate} <= {num}".format(predicate='time', num="2012"),
+ ('time', ast.Datetime(datetime.min, datetime(2013, 1, 1), False, False)))
+ self._test_number("{predicate} <= {num}".format(predicate='time', num="2012.04"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 5, 1), False, False)))
+ self._test_number("{predicate} <= {num}".format(predicate='time', num="2012.04.30"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 5, 1), False, False)))
+ self._test_number("{predicate} <= {num}".format(predicate='time', num="2012.04.30, 3 pm"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 16), False, False)))
+ self._test_number("{predicate} <= {num}".format(predicate='time', num="2012.04.30, 15:34"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 35), False, False)))
+ self._test_number("{predicate} <= {num}".format(predicate='time', num="2012.04.30, 15:34:12"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 34, 13), False, False)))
+ self._test_number("{predicate} <= {num}".format(predicate='time', num="2012.04.30, 15:34:12.98"),
+ ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 34, 12, 980001), False, False)))
+
+ def test_timerange(self):
+ raise NotImplementedError() # FIXME
+ self._test_number("{predicate} < {num}".format(predicate='time', num="15:34"),
+ ('time', ast.TimeRange(datetime.utcfromtimestamp(0.0), datetime(1970, 1, 1, 15, 34), True, False)))
+ self._test_number("{predicate} <= {num}".format(predicate='time', num="15:34"),
+ ('time', ast.TimeRange(datetime.utcfromtimestamp(0.0), datetime(1970, 1, 1, 15, 35), True, False)))
+ self._test_number("{predicate} = {num}".format(predicate='time', num="15:34"),
+ ('time', ast.TimeRange(datetime(1970, 1, 1, 15, 34), datetime(1970, 1, 1, 15, 35), True, False)))
+ self._test_number("{predicate} > {num}".format(predicate='time', num="15:34"),
+ ('time', ast.TimeRange(datetime(1970, 1, 1, 15, 35), datetime(1970, 1, 2), True, True)))
+ self._test_number("{predicate} >= {num}".format(predicate='time', num="15:34"),
+ ('time', ast.TimeRange(datetime(1970, 1, 1, 15, 34), datetime(1970, 1, 2), True, True)))
+
+ self._test_number("{numA} <= {predicate} <= {numB}".format(predicate='time', numA="12:34", numB="15:28"),
+ ('time', ast.TimeRange(datetime(1970, 1, 1, 12, 34), datetime(1970, 1, 1, 15, 29), True, False)))
+ self._test_number("{numA} <= {predicate} < {numB}".format(predicate='time', numA="12:34", numB="15:28"),
+ ('time', ast.TimeRange(datetime(1970, 1, 1, 12, 34), datetime(1970, 1, 1, 15, 28), True, False)))
+ self._test_number("{numA} < {predicate} <= {numB}".format(predicate='time', numA="12:34", numB="15:28"),
+ ('time', ast.TimeRange(datetime(1970, 1, 1, 12, 35), datetime(1970, 1, 1, 15, 29), True, False)))
+ self._test_number("{numA} < {predicate} < {numB}".format(predicate='time', numA="12:34", numB="15:28"),
+ ('time', ast.TimeRange(datetime(1970, 1, 1, 12, 35), datetime(1970, 1, 1, 15, 28), True, False)))
+
+ def test_special(self):
+ # special cases: explicit plus sign
+ self._test_number("{predicate} in [+1.23-+4.56]".format(predicate='iso'),
+ (ns.bse.iso, ast.filter.Between(1.23, 4.56, False, False)))
+ self._test_number("{predicate} in [-+4.56]".format(predicate='iso'),
+ (ns.bse.iso, ast.filter.LessThan(4.56, False)))
+
+ def test_errors(self):
+ # parse errors
+ for editable in [
+ # equal with exclusive
+ "{predicate} in ({num}:{num})", "{predicate} in ({num}-{num})",
+ "{predicate} in ({num}:{num}[", "{predicate} in ({num}-{num}[",
+ "{predicate} in ]{num}:{num})", "{predicate} in ]{num}-{num})",
+ "{predicate} in ]{num}:{num}[", "{predicate} in ]{num}-{num}[",
+ # invalid parentesis
+ "{predicate} in ){num}:{num}(",
+ # misc errors
+ # FIXME: Currently all special characters are allowed as categorical value.
+ # If this changes, don't forget to enable the tests below.
+ #"{predicate} in [{num}{num}]",
+ #"{predicate} [{num}:{num}:{num}]",
+ #"{predicate} = ({num})",
+ #"{predicate} = {num})",
+ ]:
+ self.assertRaises(errors.ParserError, self.parse,
+ editable.format(predicate='iso', num=1.23))
+
+ for editable in [
+ "{predicate} in [{numA}:{numB}]", "{predicate} : [{numA}:{numB}]", "{predicate} = [{numA}:{numB}]",
+ "{predicate} in ]{numA}:{numB}]", "{predicate} : ]{numA}:{numB}]", "{predicate} = ]{numA}:{numB}]",
+ "{predicate} in [{numA}:{numB}[", "{predicate} : [{numA}:{numB}[", "{predicate} = [{numA}:{numB}[",
+ "{predicate} in ({numA}:{numB}]", "{predicate} : ({numA}:{numB}]", "{predicate} = ({numA}:{numB}]",
+ "{predicate} in [{numA}:{numB})", "{predicate} : [{numA}:{numB})", "{predicate} = [{numA}:{numB})",
+ "{predicate} in ]{numA}:{numB}[", "{predicate} : ]{numA}:{numB}[", "{predicate} = ]{numA}:{numB}[",
+ "{predicate} in ]{numA}:{numB})", "{predicate} : ]{numA}:{numB})", "{predicate} = ]{numA}:{numB})",
+ "{predicate} in ({numA}:{numB}[", "{predicate} : ({numA}:{numB}[", "{predicate} = ({numA}:{numB}[",
+ "{predicate} in ({numA}:{numB})", "{predicate} : ({numA}:{numB})", "{predicate} = ({numA}:{numB})",
+ "{numA} < {predicate} < {numB}",
+ "{numA} <= {predicate} < {numB}",
+ "{numA} < {predicate} <= {numB}",
+ ]:
+ self.assertRaises(errors.ParserError, self.parse,
+ editable.format(predicate='iso', numA=4.56, numB=1.23))
+ # FIXME:
+ #self.assertRaises(errors.ParserError, self.parse,
+ # editable.format(predicate='time', numA="17:35", numB="10:55"))
+ #self.assertRaises(errors.ParserError, self.parse,
+ # editable.format(predicate='time', numA="18.12.2035", numB="5.7.1999"))
+
+ raise NotImplementedError() # FIXME
+ # special cases: empty range with boundary
+ self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
+ "{predicate} in [:]".format(predicate='iso'))
+ self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
+ "{predicate} in (:[".format(predicate='iso'))
+ self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
+ "{predicate} in ]:)".format(predicate='iso'))
+ self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
+ "{predicate} in ".format(predicate='iso'))
+ # misc
+ self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
+ "{predicate} in [{num}{num}]".format(predicate='iso', num=1.23))
+ self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
+ "{predicate} [{num}:{num}:{num}]".format(predicate='iso', num=1.23))
+ self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
+ "{predicate} = ({num})".format(predicate='iso', num=1.23))
+ self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
+ "{predicate} = ({num}".format(predicate='iso', num=1.23), dict(parseAll=True))
+ self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
+ "{predicate} = {num})".format(predicate='iso', num=1.23), dict(parseAll=True))
+ # range errors
+ self.assertRaises(errors.ParserError, self.parse, "100 >= iso < 200")
+ self.assertRaises(errors.ParserError, self.parse, "100 > iso < 200")
+ self.assertRaises(errors.ParserError, self.parse, "100 > iso <= 200")
+ self.assertRaises(errors.ParserError, self.parse, "100 >= iso <= 200")
+ self.assertRaises(errors.ParserError, self.parse, "100 = iso = 200")
+ # time/date mixture errors
+ self.assertRaises(errors.ParserError, self.parse, "12:45 < time < 17.5.2004")
+ self.assertRaises(errors.ParserError, self.parse, "17.5.2004 < time < 12:45")
+ # date/int mixture errors
+ self.assertRaises(errors.ParserError, self.parse, "17.5.2004 < time < 1245")
+ # 1245 is interpreted as the year
+ #self.assertRaises(errors.ParserError, self.parse, "1245 < time < 17.5.2004")
+ # time/int mixture errors
+ self.assertRaises(errors.ParserError, self.parse, "17:12 < time < 1245")
+ self.assertRaises(errors.ParserError, self.parse, "1712 < time < 12:45")
+
+ # empty query
+ self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString, "")
+
+
+
+
+ def _test(self, query, target):
+ result = self.parse(query)
+ target = ast.filter.And(target)
+ self.assertEqual(result, target, msg="in query '{}'".format(query))
+
+ def test_parse_existence(self):
+ self._test('has mime',
+ ast.filter.Has(ns.bse.mime))
+ self._test('has no mime',
+ ast.filter.Not(ast.filter.Has(ns.bse.mime)))
+ self._test('has not mime',
+ ast.filter.Not(ast.filter.Has(ns.bse.mime)))
+
+ def test_parse_categorical(self):
+ # positive
+ self._test("iso in 100, 200, 500",
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200', '500')))
+ self._test("iso in (100, 200)",
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200')))
+ self._test("iso = (100, 200)",
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200')))
+ # FIXME!
+ #self._test("iso = 100, 200",
+ # ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200')))
+ self._test("iso : (100, 200)",
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200')))
+ self._test("iso : 100, 200",
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200')))
+ self._test("iso:(100,200)",
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200')))
+ self._test("iso in (100,200)",
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200')))
+ self._test("iso in 100,200",
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200')))
+ self._test("iso ~ (100,200)",
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200', approx=True)))
+ self._test("iso ~ 100,200",
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('100', '200', approx=True)))
+
+ # negative
+ self._test("iso not in 100,200",
+ ast.filter.All(ns.bse.iso, ast.filter.Excludes('100', '200')))
+ self._test("iso not in (100, 200)",
+ ast.filter.All(ns.bse.iso, ast.filter.Excludes('100', '200')))
+ self._test("iso != 100,200",
+ ast.filter.All(ns.bse.iso, ast.filter.Excludes('100', '200')))
+ self._test("iso != (100, 200)",
+ ast.filter.All(ns.bse.iso, ast.filter.Excludes('100', '200')))
+ self._test("iso !~ 100,200",
+ ast.filter.All(ns.bse.iso, ast.filter.Excludes('100', '200', approx=True)))
+ self._test("iso !~ (100, 200)",
+ ast.filter.All(ns.bse.iso, ast.filter.Excludes('100', '200', approx=True)))
+
+ # one value
+ self._test("mime : text",
+ ast.filter.Any(ns.bse.mime, ast.filter.Includes('text')))
+ self._test("mime in text",
+ ast.filter.Any(ns.bse.mime, ast.filter.Includes('text')))
+ self._test("mime = text",
+ ast.filter.Any(ns.bse.mime, ast.filter.Includes('text')))
+ self._test("mime ~ text",
+ ast.filter.Any(ns.bse.mime, ast.filter.Includes('text', approx=True)))
+ self._test("mime != text",
+ ast.filter.All(ns.bse.mime, ast.filter.Excludes('text')))
+ self._test("mime not in text",
+ ast.filter.All(ns.bse.mime, ast.filter.Excludes('text')))
+ self._test("mime !~ text",
+ ast.filter.All(ns.bse.mime, ast.filter.Excludes('text', approx=True)))
+
+ # expressions with slash and comma
+ self._test('mime : "text"',
+ ast.filter.Any(ns.bse.mime, ast.filter.Includes('text')))
+ self._test('mime : "text", "plain"',
+ ast.filter.Any(ns.bse.mime, ast.filter.Includes('text', 'plain')))
+ self._test('mime : "text, plain"',
+ ast.filter.Any(ns.bse.mime, ast.filter.Includes('text, plain')))
+ self._test('mime ~ "text/plain"',
+ ast.filter.Any(ns.bse.mime, ast.filter.Includes('text/plain', approx=True)))
+ self._test('mime = ("text/plain", "image/jpeg")',
+ ast.filter.Any(ns.bse.mime, ast.filter.Includes('text/plain', 'image/jpeg')))
+
+ def test_parse_tag(self):
+ # only tag: tag, tags, (tag), (tags)
+ self._test("foo",
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Equals('foo'))))
+ self._test("(foo)",
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Equals('foo'))))
+ self._test("foo, bar",
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Includes('foo', 'bar'))))
+ self._test("foo,bar",
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Includes('foo', 'bar'))))
+ self._test("(foo, bar,foobar)",
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Includes('foo', 'bar', 'foobar'))))
+
+ # op and tag: !tag, ~tag, !~tag
+ self._test("~foo",
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Substring('foo'))))
+ self._test("~ foo",
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Substring('foo'))))
+ self._test("!foo",
+ ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Not(ast.filter.Equals('foo')))))
+ self._test("! foo",
+ ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Not(ast.filter.Equals('foo')))))
+ self._test("!~foo",
+ ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Not(ast.filter.Substring('foo')))))
+ self._test("!~ foo",
+ ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Not(ast.filter.Substring('foo')))))
+
+ # op and list: ! (tags), ~tags, ...
+ self._test("~ foo, bar",
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Includes('foo', 'bar', approx=True))))
+ self._test("~foo, bar",
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Includes('foo', 'bar', approx=True))))
+ self._test("~ (foo, bar)",
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Includes('foo', 'bar', approx=True))))
+ self._test("! foo, bar",
+ ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Excludes('foo', 'bar'))))
+ self._test("! (foo, bar)",
+ ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Excludes('foo', 'bar'))))
+ self._test("! (foo,bar)",
+ ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Excludes('foo', 'bar'))))
+ self._test("!~ foo, bar",
+ ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Excludes('foo', 'bar', approx=True))))
+ self._test("!~ (foo, bar)",
+ ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Excludes('foo', 'bar', approx=True))))
+ self._test("!~(foo,bar)",
+ ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Excludes('foo', 'bar', approx=True))))
+
+ def test_parse_query(self):
+ # simple query
+ self.assertEqual(self.parse('foo / bar'), ast.filter.And(
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Equals('foo'))),
+ ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Equals('bar')))))
+ self.assertEqual(self.parse('iso in ("foo", "bar") / mime = plain'), ast.filter.And(
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('foo', 'bar')),
+ ast.filter.Any(ns.bse.mime, ast.filter.Equals('plain'))))
+ self.assertEqual(self.parse('iso in ("foo", "bar") / mime = plain'), ast.filter.And(
+ ast.filter.Any(ns.bse.iso, ast.filter.Includes('foo', 'bar')),
+ ast.filter.Any(ns.bse.mime, ast.filter.Equals('plain'))))
+ self.assertEqual(self.parse('iso = 1.23 / rank < 5'), ast.filter.And(
+ ast.filter.Any(ns.bse.iso, ast.filter.Equals(1.23)),
+ ast.filter.Any(ns.bse.rank, ast.filter.LessThan(5, True))))
+ # FIXME
+ #self.assertEqual(self.parse('time >= 12:50 / time < 13:50'), ast.filter.And(
+ # ast.filter.Any(ns.bse.time, ast.TimeRange(lo=datetime(1970, 1, 1, 12, 50), lo_inc=True, hi_inc=True)),
+ # ast.filter.Any(ns.bse.time, ast.TimeRange(hi=datetime(1970, 1, 1, 13, 50), lo_inc=True, hi_inc=False))))
+ #self.assertEqual(self.parse('time >= 17.5.2001 / time < 18.4.2002'), ast.filter.And(
+ # ast.filter.Any(ns.bse.time, ast.Datetime(lo=datetime(2001, 5, 17, 0, 0), lo_inc=True)),
+ # ast.filter.Any(ns.bse.time, ast.Datetime(hi=datetime(2002, 4, 18, 0, 0)))))
+ # mixing expressions
+ #self.assertEqual(self.parse('foo / iso in "bar" / mime ~ "text/plain" / iso < 100 / time >= 17.5.2001 / time < 13:50'), ast.filter.And(
+ # ast.filter.Any(ns.bse.tag, ast.filter.Equals('foo')),
+ # ast.filter.Any(ns.bse.iso, ast.filter.Equals('bar')),
+ # ast.filter.Any(ns.bse.mime, ast.filter.Substring('text/plain')),
+ # ast.filter.Any(ns.bse.iso, ast.filter.LessThan(100)),
+ # ast.filter.Any(ns.bse.time, ast.Datetime(lo=datetime(2001, 5, 17, 0, 0), lo_inc=True)),
+ # ast.filter.Any(ns.bse.time, ast.TimeRange(hi=datetime(1970, 1, 1, 13, 50), lo_inc=True))))
+
+ # leading/trailing slashes
+ self.assertRaises(errors.ParserError, self.parse, '/ foobar')
+ self.assertRaises(errors.ParserError, self.parse, 'foobar /')
+ self.assertRaises(errors.ParserError, self.parse, 'foobar / ')
+ self.assertRaises(errors.ParserError, self.parse, 'foo // bar')
+ self.assertRaises(errors.ParserError, self.parse, 'foo / / bar')
+
+ def test_quoting(self):
+ self._test("tag in ('(foo, bar)', foobar)",
+ ast.filter.Any(ns.bse.tag, ast.filter.Includes('(foo, bar)', 'foobar')))
+ self._test('tag in ("(foo, bar)", foobar)',
+ ast.filter.Any(ns.bse.tag, ast.filter.Includes('(foo, bar)', 'foobar')))
+ self._test('tag in ("(foo, \\"bar\\")", foobar)',
+ ast.filter.Any(ns.bse.tag, ast.filter.Includes('(foo, "bar")', 'foobar')))
+ self._test('tag in ("(foo, bar)", "foobar")',
+ ast.filter.Any(ns.bse.tag, ast.filter.Includes('(foo, bar)', 'foobar')))
+ self._test('tag in ("(foo, bar)", \'foobar\')',
+ ast.filter.Any(ns.bse.tag, ast.filter.Includes('(foo, bar)', 'foobar')))
+
+ # error cases
+ self.assertRaises(errors.ParserError, self.parse, ('tag in ("(foo, bar, foobar)'))
+ self.assertRaises(errors.ParserError, self.parse, ("tag in ('(foo, bar, foobar)"))
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/parsing/test_search.py b/test/parsing/test_search.py
deleted file mode 100644
index 23801d0..0000000
--- a/test/parsing/test_search.py
+++ /dev/null
@@ -1,707 +0,0 @@
-"""
-
-Part of the tagit test suite.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
-# standard imports
-import unittest
-from datetime import datetime
-
-# external imports
-from pyparsing import ParseException
-
-# tagit imports
-from tagit.utils import errors
-#from tagit.parsing.search import ast, predicates, PredicateScope # FIXME: mb/port/parsing
-
-# objects to test
-from tagit.parsing.search import ast_from_string
-
-
-## code ##
-
-class TestScope(PredicateScope):
- _scope_order = ['major', 'minor', 'micro']
- _init_values = ['library']
-
-
-class TestParseContinuous(unittest.TestCase):
- longMessage = True
-
- def setUp(self):
- predicates.expose('mime',
- TestScope('attribute', 'mime'), 'Categorical')
- predicates.expose('iso',
- TestScope('attribute', 'iso'), 'Continuous', 'Categorical')
- predicates.expose('time',
- TestScope('generic', 't_image_create_loc'), 'TimeRange', 'Datetime')
- predicates.expose('tag',
- TestScope('generic', 'tag'), 'Categorical')
-
- def _test(self, query, target):
- predicate, condition = target
- result = ast_from_string(query)
- target = ast.AND([ast.Token(predicate, condition)])
- self.assertEqual(result, target, msg="in query '{}'".format(query))
-
- def test_larger_than(self):
- # larger than A (inclusive)
- for editable in [
- # range
- "{predicate} in [{num}:]", "{predicate} in [{num}:[", "{predicate} in [{num}:)",
- "{predicate} : [{num}:]", "{predicate} : [{num}:[", "{predicate} : [{num}:)",
- "{predicate} = [{num}:]", "{predicate} = [{num}:[", "{predicate} = [{num}:)",
- ]:
- # positive
- self._test(editable.format(num=1.23, predicate='iso'),
- ('iso', ast.Continuous(1.23, float('inf'), True, False)))
- # negative
- self._test(editable.format(num=-1.23, predicate='iso'),
- ('iso', ast.Continuous(-1.23, float('inf'), True, False)))
-
- for editable in [
- # range
- "{predicate} in [{num}-]", "{predicate} in [{num}-[", "{predicate} in [{num}-)",
- "{predicate} : [{num}-]", "{predicate} : [{num}-[", "{predicate} : [{num}-)",
- "{predicate} = [{num}-]", "{predicate} = [{num}-[", "{predicate} = [{num}-)",
- # equation
- "{predicate} >= {num}", "{num} <= {predicate}",
- ]:
- # positive
- self._test(editable.format(num=1.23, predicate='iso'),
- ('iso', ast.Continuous(1.23, float('inf'), True, False)))
- # negative
- self._test(editable.format(num=-1.23, predicate='iso'),
- ('iso', ast.Continuous(-1.23, float('inf'), True, False)))
- # date
- self._test(editable.format(predicate='time', num="30.04.2012, 13:18"),
- ('time', ast.Datetime(datetime(2012, 4, 30, 13, 18), datetime.max, True, False)))
-
- # larger than A (exclusive)
- for editable in [
- # range / bracket
- "{predicate} in ]{num}:]", "{predicate} in ]{num}:[", "{predicate} in ]{num}:)",
- "{predicate} : ]{num}:]", "{predicate} : ]{num}:[", "{predicate} : ]{num}:)",
- "{predicate} = ]{num}:]", "{predicate} = ]{num}:[", "{predicate} = ]{num}:)",
- # range / parenthesis
- "{predicate} in ({num}:]", "{predicate} in ({num}:[", "{predicate} in ({num}:)",
- "{predicate} : ({num}:]", "{predicate} : ({num}:[", "{predicate} : ({num}:)",
- "{predicate} = ({num}:]", "{predicate} = ({num}:[", "{predicate} = ({num}:)",
- ]:
- # positive
- self._test(editable.format(num=1.23, predicate='iso'),
- ('iso', ast.Continuous(1.23, float('inf'), False, False)))
- # negative
- self._test(editable.format(num=-1.23, predicate='iso'),
- ('iso', ast.Continuous(-1.23, float('inf'), False, False)))
-
- for editable in [
- # range / bracket
- "{predicate} in ]{num}-]", "{predicate} in ]{num}-[", "{predicate} in ]{num}-)",
- "{predicate} : ]{num}-]", "{predicate} : ]{num}-[", "{predicate} : ]{num}-)",
- "{predicate} = ]{num}-]", "{predicate} = ]{num}-[", "{predicate} = ]{num}-)",
- # range / parenthesis
- "{predicate} in ({num}-]", "{predicate} in ({num}-[", "{predicate} in ({num}-)",
- "{predicate} : ({num}-]", "{predicate} : ({num}-[", "{predicate} : ({num}-)",
- "{predicate} = ({num}-]", "{predicate} = ({num}-[", "{predicate} = ({num}-)",
- # equation
- "{predicate} > {num}", "{num} < {predicate}",
- ]:
- # positive
- self._test(editable.format(num=1.23, predicate='iso'),
- ('iso', ast.Continuous(1.23, float('inf'), False, False)))
- # negative
- self._test(editable.format(num=-1.23, predicate='iso'),
- ('iso', ast.Continuous(-1.23, float('inf'), False, False)))
- # date
- self._test(editable.format(predicate='time', num="30.04.2012, 13:18"),
- ('time', ast.Datetime(datetime(2012, 4, 30, 13, 19), datetime.max, True, False)))
-
- def test_smaller_than(self):
- # smaller than B (inclusive)
- for editable in [
- # range
- "{predicate} in [:{num}]", "{predicate} in (:{num}]", "{predicate} in ]:{num}]",
- "{predicate} : [:{num}]", "{predicate} : (:{num}]", "{predicate} : ]:{num}]",
- "{predicate} = [:{num}]", "{predicate} = (:{num}]", "{predicate} = ]:{num}]",
- ]:
- # positives
- self._test(editable.format(num=1.23, predicate='iso'),
- ('iso', ast.Continuous(float('-inf'), 1.23, False, True)))
- # negatives
- self._test(editable.format(num=-1.23, predicate='iso'),
- ('iso', ast.Continuous(float('-inf'), -1.23, False, True)))
-
- for editable in [
- # range
- "{predicate} in [-{num}]", "{predicate} in (-{num}]", "{predicate} in ]-{num}]",
- "{predicate} : [-{num}]", "{predicate} : (-{num}]", "{predicate} : ]-{num}]",
- "{predicate} = [-{num}]", "{predicate} = (-{num}]", "{predicate} = ]-{num}]",
- # equation
- "{predicate} <={num}", "{num} >= {predicate}",
- ]:
- # positives
- self._test(editable.format(num=1.23, predicate='iso'),
- ('iso', ast.Continuous(float('-inf'), 1.23, False, True)))
- # negatives
- self._test(editable.format(num=-1.23, predicate='iso'),
- ('iso', ast.Continuous(float('-inf'), -1.23, False, True)))
- # date
- self._test(editable.format(predicate='time', num="30.04.2012, 13:18"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 13, 19), False, False)))
-
- # smaller than B (exclusive)
- for editable in [
- # range / bracket
- "{predicate} in [:{num}[", "{predicate} in (:{num}[", "{predicate} in ]:{num}[",
- "{predicate} : [:{num}[", "{predicate} : (:{num}[", "{predicate} : ]:{num}[",
- "{predicate} = [:{num}[", "{predicate} = (:{num}[", "{predicate} = ]:{num}[",
- # range / parenthesis
- "{predicate} in [:{num})", "{predicate} in (:{num})", "{predicate} in ]:{num})",
- "{predicate} : [:{num})", "{predicate} : (:{num})", "{predicate} : ]:{num})",
- "{predicate} = [:{num})", "{predicate} = (:{num})", "{predicate} = ]:{num})",
- ]:
- # positives
- self._test(editable.format(num=1.23, predicate='iso'),
- ('iso', ast.Continuous(float('-inf'), 1.23, False, False)))
- # negatives
- self._test(editable.format(num=-1.23, predicate='iso'),
- ('iso', ast.Continuous(float('-inf'), -1.23, False, False)))
-
- for editable in [
- # range / bracket
- "{predicate} in [-{num}[", "{predicate} in (-{num}[", "{predicate} in ]-{num}[",
- "{predicate} : [-{num}[", "{predicate} : (-{num}[", "{predicate} : ]-{num}[",
- "{predicate} = [-{num}[", "{predicate} = (-{num}[", "{predicate} = ]-{num}[",
- # range / parenthesis
- "{predicate} in [-{num})", "{predicate} in (-{num})", "{predicate} in ]-{num})",
- "{predicate} : [-{num})", "{predicate} : (-{num})", "{predicate} : ]-{num})",
- "{predicate} = [-{num})", "{predicate} = (-{num})", "{predicate} = ]-{num})",
- # equation
- "{predicate} <{num}", "{num} > {predicate}",
- ]:
- # positives
- self._test(editable.format(num=1.23, predicate='iso'),
- ('iso', ast.Continuous(float('-inf'), 1.23, False, False)))
- # negatives
- self._test(editable.format(num=-1.23, predicate='iso'),
- ('iso', ast.Continuous(float('-inf'), -1.23, False, False)))
- # date
- self._test(editable.format(predicate='time', num="30.04.2012, 13:18"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 13, 18), False, False)))
-
- def test_between(self):
- # between A and B (including A, including B)
- for editable in [
- # range
- "{predicate} in [{numA}:{numB}]", "{predicate} : [{numA}:{numB}]", "{predicate} = [{numA}:{numB}]",
- ]:
- # positives
- self._test(editable.format(predicate='iso', numA=1.23, numB=4.56),
- ('iso', ast.Continuous(1.23, 4.56, True, True)))
- # negatives
- self._test(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
- ('iso', ast.Continuous(-4.56, -1.23, True, True)))
- # mixed
- self._test(editable.format(predicate='iso', numA=-1.23, numB=4.56),
- ('iso', ast.Continuous(-1.23, 4.56, True, True)))
-
- for editable in [
- # range
- "{predicate} in [{numA}-{numB}]", "{predicate} : [{numA}-{numB}]", "{predicate} = [{numA}-{numB}]",
- # equation
- "{numA} <= {predicate} <= {numB}"
- ]:
- # positives
- self._test(editable.format(predicate='iso', numA=1.23, numB=4.56),
- ('iso', ast.Continuous(1.23, 4.56, True, True)))
- # negatives
- self._test(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
- ('iso', ast.Continuous(-4.56, -1.23, True, True)))
- # mixed
- self._test(editable.format(predicate='iso', numA=-1.23, numB=4.56),
- ('iso', ast.Continuous(-1.23, 4.56, True, True)))
- # date
- self._test(editable.format(predicate='time', numA="30.04.2012, 13:18", numB="13.6.2014, 18:27"),
- ('time', ast.Datetime(datetime(2012, 4, 30, 13, 18), datetime(2014, 6, 13, 18, 28), True, False)))
-
- # between A and B (including A, excluding B)
- for editable in [
- # range
- "{predicate} in [{numA}:{numB})", "{predicate} in [{numA}:{numB}[",
- "{predicate} : [{numA}:{numB})", "{predicate} : [{numA}:{numB}[",
- "{predicate} = [{numA}:{numB})", "{predicate} = [{numA}:{numB}[",
- ]:
- # positives
- self._test(editable.format(predicate='iso', numA=1.23, numB=4.56),
- ('iso', ast.Continuous(1.23, 4.56, True, False)))
- # negatives
- self._test(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
- ('iso', ast.Continuous(-4.56, -1.23, True, False)))
- # mixed
- self._test(editable.format(predicate='iso', numA=-1.23, numB=4.56),
- ('iso', ast.Continuous(-1.23, 4.56, True, False)))
-
- for editable in [
- # range
- "{predicate} in [{numA}-{numB})", "{predicate} in [{numA}-{numB}[",
- "{predicate} : [{numA}-{numB})", "{predicate} : [{numA}-{numB}[",
- "{predicate} = [{numA}-{numB})", "{predicate} = [{numA}-{numB}[",
- # equation
- "{numA} <= {predicate} < {numB}",
- ]:
- # positives
- self._test(editable.format(predicate='iso', numA=1.23, numB=4.56),
- ('iso', ast.Continuous(1.23, 4.56, True, False)))
- # negatives
- self._test(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
- ('iso', ast.Continuous(-4.56, -1.23, True, False)))
- # mixed
- self._test(editable.format(predicate='iso', numA=-1.23, numB=4.56),
- ('iso', ast.Continuous(-1.23, 4.56, True, False)))
- # date
- self._test(editable.format(predicate='time', numA="30.04.2012, 13:18", numB="13.6.2014, 18:27"),
- ('time', ast.Datetime(datetime(2012, 4, 30, 13, 18), datetime(2014, 6, 13, 18, 27), True, False)))
-
- # between A and B (excluding A, including B)
- for editable in [
- # range
- "{predicate} in ({numA}:{numB}]", "{predicate} in ]{numA}:{numB}]",
- "{predicate} : ({numA}:{numB}]", "{predicate} : ]{numA}:{numB}]",
- "{predicate} = ({numA}:{numB}]", "{predicate} = ]{numA}:{numB}]",
- ]:
- # positives
- self._test(editable.format(predicate='iso', numA=1.23, numB=4.56),
- ('iso', ast.Continuous(1.23, 4.56, False, True)))
- # negatives
- self._test(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
- ('iso', ast.Continuous(-4.56, -1.23, False, True)))
- # mixed
- self._test(editable.format(predicate='iso', numA=-1.23, numB=4.56),
- ('iso', ast.Continuous(-1.23, 4.56, False, True)))
-
- for editable in [
- # range
- "{predicate} in ({numA}-{numB}]", "{predicate} in ]{numA}-{numB}]",
- "{predicate} : ({numA}-{numB}]", "{predicate} : ]{numA}-{numB}]",
- "{predicate} = ({numA}-{numB}]", "{predicate} = ]{numA}-{numB}]",
- # equation
- "{numA} < {predicate} <= {numB}",
- ]:
- # positives
- self._test(editable.format(predicate='iso', numA=1.23, numB=4.56),
- ('iso', ast.Continuous(1.23, 4.56, False, True)))
- # negatives
- self._test(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
- ('iso', ast.Continuous(-4.56, -1.23, False, True)))
- # mixed
- self._test(editable.format(predicate='iso', numA=-1.23, numB=4.56),
- ('iso', ast.Continuous(-1.23, 4.56, False, True)))
- # date
- self._test(editable.format(predicate='time', numA="30.04.2012, 13:18", numB="13.6.2014, 18:27"),
- ('time', ast.Datetime(datetime(2012, 4, 30, 13, 19), datetime(2014, 6, 13, 18, 28), True, False)))
-
- # between A and B (excluding A, excluding B)
- for editable in [
- "{predicate} in ({numA}:{numB})", "{predicate} in ]{numA}:{numB}[",
- "{predicate} : ({numA}:{numB})", "{predicate} : ]{numA}:{numB}[",
- "{predicate} = ({numA}:{numB})", "{predicate} = ]{numA}:{numB}[",
- ]:
- # positives
- self._test(editable.format(predicate='iso', numA=1.23, numB=4.56),
- ('iso', ast.Continuous(1.23, 4.56, False, False)))
- # negatives
- self._test(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
- ('iso', ast.Continuous(-4.56, -1.23, False, False)))
- # mixed
- self._test(editable.format(predicate='iso', numA=-1.23, numB=4.56),
- ('iso', ast.Continuous(-1.23, 4.56, False, False)))
-
- for editable in [
- "{predicate} in ({numA}-{numB})", "{predicate} in ]{numA}-{numB}[",
- "{predicate} : ({numA}-{numB})", "{predicate} : ]{numA}-{numB}[",
- "{predicate} = ({numA}-{numB})", "{predicate} = ]{numA}-{numB}[",
- # equation
- "{numA} < {predicate} < {numB}",
- ]:
- # positives
- self._test(editable.format(predicate='iso', numA=1.23, numB=4.56),
- ('iso', ast.Continuous(1.23, 4.56, False, False)))
- # negatives
- self._test(editable.format(predicate='iso', numA=-4.56, numB=-1.23),
- ('iso', ast.Continuous(-4.56, -1.23, False, False)))
- # mixed
- self._test(editable.format(predicate='iso', numA=-1.23, numB=4.56),
- ('iso', ast.Continuous(-1.23, 4.56, False, False)))
- # date
- self._test(editable.format(predicate='time', numA="30.04.2012, 13:18", numB="13.6.2014, 18:27"),
- ('time', ast.Datetime(datetime(2012, 4, 30, 13, 19), datetime(2014, 6, 13, 18, 27), True, False)))
-
- def test_equal(self):
- # equal to A
- for editable in [
- # range
- "{predicate} in [{num}:{num}]", "{predicate} : [{num}:{num}]", "{predicate} = [{num}:{num}]",
- ]:
- # positives
- self._test(editable.format(predicate='iso', num=1.23),
- ('iso', ast.Continuous(1.23, 1.23, True, True)))
- # negatives
- self._test(editable.format(predicate='iso', num=-1.23),
- ('iso', ast.Continuous(-1.23, -1.23, True, True)))
-
- for editable in [
- # range
- "{predicate} in [{num}-{num}]", "{predicate} : [{num}-{num}]", "{predicate} = [{num}-{num}]",
- # equation
- "{predicate} = {num}", "{num} = {predicate}",
- ]:
- # positives
- self._test(editable.format(predicate='iso', num=1.23),
- ('iso', ast.Continuous(1.23, 1.23, True, True)))
- # negatives
- self._test(editable.format(predicate='iso', num=-1.23),
- ('iso', ast.Continuous(-1.23, -1.23, True, True)))
- # date
- self._test(editable.format(predicate='time', num="30.04.2012, 13:18"),
- ('time', ast.Datetime(datetime(2012, 4, 30, 13, 18), datetime(2012, 4, 30, 13, 19), True, False)))
-
- def test_dates(self):
- self._test("{predicate} < {num}".format(predicate='time', num="2012"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 1, 1), False, False)))
- self._test("{predicate} < {num}".format(predicate='time', num="2012.04"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 1), False, False)))
- self._test("{predicate} < {num}".format(predicate='time', num="2012.04.30"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30), False, False)))
- self._test("{predicate} < {num}".format(predicate='time', num="2012.04.30, 3 pm"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15), False, False)))
- self._test("{predicate} < {num}".format(predicate='time', num="2012.04.30, 15:34"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 34), False, False)))
- self._test("{predicate} < {num}".format(predicate='time', num="2012.04.30, 15:34:12"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 34, 12), False, False)))
- self._test("{predicate} < {num}".format(predicate='time', num="2012.04.30, 15:34:12.98"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 34, 12, 980000), False, False)))
-
- self._test("{predicate} <= {num}".format(predicate='time', num="2012"),
- ('time', ast.Datetime(datetime.min, datetime(2013, 1, 1), False, False)))
- self._test("{predicate} <= {num}".format(predicate='time', num="2012.04"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 5, 1), False, False)))
- self._test("{predicate} <= {num}".format(predicate='time', num="2012.04.30"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 5, 1), False, False)))
- self._test("{predicate} <= {num}".format(predicate='time', num="2012.04.30, 3 pm"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 16), False, False)))
- self._test("{predicate} <= {num}".format(predicate='time', num="2012.04.30, 15:34"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 35), False, False)))
- self._test("{predicate} <= {num}".format(predicate='time', num="2012.04.30, 15:34:12"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 34, 13), False, False)))
- self._test("{predicate} <= {num}".format(predicate='time', num="2012.04.30, 15:34:12.98"),
- ('time', ast.Datetime(datetime.min, datetime(2012, 4, 30, 15, 34, 12, 980001), False, False)))
-
- def test_timerange(self):
- self._test("{predicate} < {num}".format(predicate='time', num="15:34"),
- ('time', ast.TimeRange(datetime.utcfromtimestamp(0.0), datetime(1970, 1, 1, 15, 34), True, False)))
- self._test("{predicate} <= {num}".format(predicate='time', num="15:34"),
- ('time', ast.TimeRange(datetime.utcfromtimestamp(0.0), datetime(1970, 1, 1, 15, 35), True, False)))
- self._test("{predicate} = {num}".format(predicate='time', num="15:34"),
- ('time', ast.TimeRange(datetime(1970, 1, 1, 15, 34), datetime(1970, 1, 1, 15, 35), True, False)))
- self._test("{predicate} > {num}".format(predicate='time', num="15:34"),
- ('time', ast.TimeRange(datetime(1970, 1, 1, 15, 35), datetime(1970, 1, 2), True, True)))
- self._test("{predicate} >= {num}".format(predicate='time', num="15:34"),
- ('time', ast.TimeRange(datetime(1970, 1, 1, 15, 34), datetime(1970, 1, 2), True, True)))
-
- self._test("{numA} <= {predicate} <= {numB}".format(predicate='time', numA="12:34", numB="15:28"),
- ('time', ast.TimeRange(datetime(1970, 1, 1, 12, 34), datetime(1970, 1, 1, 15, 29), True, False)))
- self._test("{numA} <= {predicate} < {numB}".format(predicate='time', numA="12:34", numB="15:28"),
- ('time', ast.TimeRange(datetime(1970, 1, 1, 12, 34), datetime(1970, 1, 1, 15, 28), True, False)))
- self._test("{numA} < {predicate} <= {numB}".format(predicate='time', numA="12:34", numB="15:28"),
- ('time', ast.TimeRange(datetime(1970, 1, 1, 12, 35), datetime(1970, 1, 1, 15, 29), True, False)))
- self._test("{numA} < {predicate} < {numB}".format(predicate='time', numA="12:34", numB="15:28"),
- ('time', ast.TimeRange(datetime(1970, 1, 1, 12, 35), datetime(1970, 1, 1, 15, 28), True, False)))
-
- def test_special(self):
- # special cases: explicit plus sign
- self._test("{predicate} in [+1.23-+4.56]".format(predicate='iso'),
- ('iso', ast.Continuous(1.23, 4.56, True, True)))
- self._test("{predicate} in [-+4.56]".format(predicate='iso'),
- ('iso', ast.Continuous(float('-inf'), 4.56, False, True)))
-
- def test_errors(self):
- # parse errors
- for editable in [
- # equal with exclusive
- "{predicate} in ({num}:{num})", "{predicate} in ({num}-{num})",
- "{predicate} in ({num}:{num}[", "{predicate} in ({num}-{num}[",
- "{predicate} in ]{num}:{num})", "{predicate} in ]{num}-{num})",
- "{predicate} in ]{num}:{num}[", "{predicate} in ]{num}-{num}[",
- # invalid parentesis
- "{predicate} in ){num}:{num}(",
- # misc errors
- # FIXME: Currently all special characters are allowed as categorical value.
- # If this changes, don't forget to enable the tests below.
- #"{predicate} in [{num}{num}]",
- #"{predicate} [{num}:{num}:{num}]",
- #"{predicate} = ({num})",
- #"{predicate} = {num})",
- ]:
- self.assertRaises(errors.ParserError, ast_from_string,
- editable.format(predicate='iso', num=1.23))
-
- for editable in [
- "{predicate} in [{numA}:{numB}]", "{predicate} : [{numA}:{numB}]", "{predicate} = [{numA}:{numB}]",
- "{predicate} in ]{numA}:{numB}]", "{predicate} : ]{numA}:{numB}]", "{predicate} = ]{numA}:{numB}]",
- "{predicate} in [{numA}:{numB}[", "{predicate} : [{numA}:{numB}[", "{predicate} = [{numA}:{numB}[",
- "{predicate} in ({numA}:{numB}]", "{predicate} : ({numA}:{numB}]", "{predicate} = ({numA}:{numB}]",
- "{predicate} in [{numA}:{numB})", "{predicate} : [{numA}:{numB})", "{predicate} = [{numA}:{numB})",
- "{predicate} in ]{numA}:{numB}[", "{predicate} : ]{numA}:{numB}[", "{predicate} = ]{numA}:{numB}[",
- "{predicate} in ]{numA}:{numB})", "{predicate} : ]{numA}:{numB})", "{predicate} = ]{numA}:{numB})",
- "{predicate} in ({numA}:{numB}[", "{predicate} : ({numA}:{numB}[", "{predicate} = ({numA}:{numB}[",
- "{predicate} in ({numA}:{numB})", "{predicate} : ({numA}:{numB})", "{predicate} = ({numA}:{numB})",
- "{numA} < {predicate} < {numB}",
- "{numA} <= {predicate} < {numB}",
- "{numA} < {predicate} <= {numB}",
- ]:
- self.assertRaises(errors.ParserError, ast_from_string,
- editable.format(predicate='iso', numA=4.56, numB=1.23))
- self.assertRaises(errors.ParserError, ast_from_string,
- editable.format(predicate='time', numA="17:35", numB="10:55"))
- self.assertRaises(errors.ParserError, ast_from_string,
- editable.format(predicate='time', numA="18.12.2035", numB="5.7.1999"))
-
- # special cases: empty range with boundary
- self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
- "{predicate} in [:]".format(predicate='iso'))
- self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
- "{predicate} in (:[".format(predicate='iso'))
- self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
- "{predicate} in ]:)".format(predicate='iso'))
- self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
- "{predicate} in ".format(predicate='iso'))
- # misc
- self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
- "{predicate} in [{num}{num}]".format(predicate='iso', num=1.23))
- self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
- "{predicate} [{num}:{num}:{num}]".format(predicate='iso', num=1.23))
- self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
- "{predicate} = ({num})".format(predicate='iso', num=1.23))
- self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
- "{predicate} = ({num}".format(predicate='iso', num=1.23), dict(parseAll=True))
- self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString,
- "{predicate} = {num})".format(predicate='iso', num=1.23), dict(parseAll=True))
- # range errors
- self.assertRaises(errors.ParserError, ast_from_string, "100 >= iso < 200")
- self.assertRaises(errors.ParserError, ast_from_string, "100 > iso < 200")
- self.assertRaises(errors.ParserError, ast_from_string, "100 > iso <= 200")
- self.assertRaises(errors.ParserError, ast_from_string, "100 >= iso <= 200")
- self.assertRaises(errors.ParserError, ast_from_string, "100 = iso = 200")
- # time/date mixture errors
- self.assertRaises(errors.ParserError, ast_from_string, "12:45 < time < 17.5.2004")
- self.assertRaises(errors.ParserError, ast_from_string, "17.5.2004 < time < 12:45")
- # date/int mixture errors
- self.assertRaises(errors.ParserError, ast_from_string, "17.5.2004 < time < 1245")
- # 1245 is interpreted as the year
- #self.assertRaises(errors.ParserError, ast_from_string, "1245 < time < 17.5.2004")
- # time/int mixture errors
- self.assertRaises(errors.ParserError, ast_from_string, "17:12 < time < 1245")
- self.assertRaises(errors.ParserError, ast_from_string, "1712 < time < 12:45")
-
- # empty query
- self.assertRaises(ParseException, ast_from_string.CONTINUOUS.parseString, "")
-
-
-class TestParseSearch(unittest.TestCase):
- def setUp(self):
- predicates.expose('mime',
- TestScope('attribute', 'mime'), 'Categorical')
- predicates.expose('rank',
- TestScope('attribute', 'rank'), 'Continuous')
- predicates.expose('iso',
- TestScope('attribute', 'iso'), 'Continuous', 'Categorical')
- predicates.expose('time',
- TestScope('generic', 't_image_create_loc'), 'TimeRange', 'Datetime')
- predicates.expose('tag',
- TestScope('generic', 'tag'), 'Categorical')
-
- def test_parse_existence(self):
- self.assertEqual(ast_from_string("has mime"),
- ast.AND([ast.Token('mime', ast.Existence())]))
- self.assertEqual(ast_from_string("has no mime"),
- ast.AND([ast.Token('mime', ast.Inexistence())]))
- self.assertEqual(ast_from_string("has not mime"),
- ast.AND([ast.Token('mime', ast.Inexistence())]))
-
- def test_parse_categorical(self):
- # positive
- self.assertEqual(ast_from_string("iso in 100, 200, 500"),
- ast.AND([ast.Token('iso', ast.SetInclude(['100', '200', '500']))]))
- self.assertEqual(ast_from_string("iso in (100, 200)"),
- ast.AND([ast.Token('iso', ast.SetInclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso = (100, 200)"),
- ast.AND([ast.Token('iso', ast.SetInclude(['100', '200']))]))
- # FIXME!
- #self.assertEqual(ast_from_string("iso = 100, 200"),
- # ast.AND([ast.Token('iso', ast.SetInclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso : (100, 200)"),
- ast.AND([ast.Token('iso', ast.SetInclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso : 100, 200"),
- ast.AND([ast.Token('iso', ast.SetInclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso:(100,200)"),
- ast.AND([ast.Token('iso', ast.SetInclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso in (100,200)"),
- ast.AND([ast.Token('iso', ast.SetInclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso in 100,200"),
- ast.AND([ast.Token('iso', ast.SetInclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso ~ (100,200)"),
- ast.AND([ast.Token('iso', ast.SetInclude(['100', '200'], approximate=True))]))
- self.assertEqual(ast_from_string("iso ~ 100,200"),
- ast.AND([ast.Token('iso', ast.SetInclude(['100', '200'], approximate=True))]))
-
- # negative
- self.assertEqual(ast_from_string("iso not in 100,200"),
- ast.AND([ast.Token('iso', ast.SetExclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso not in (100, 200)"),
- ast.AND([ast.Token('iso', ast.SetExclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso != 100,200"),
- ast.AND([ast.Token('iso', ast.SetExclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso != (100, 200)"),
- ast.AND([ast.Token('iso', ast.SetExclude(['100', '200']))]))
- self.assertEqual(ast_from_string("iso !~ 100,200"),
- ast.AND([ast.Token('iso', ast.SetExclude(['100', '200'], approximate=True))]))
- self.assertEqual(ast_from_string("iso !~ (100, 200)"),
- ast.AND([ast.Token('iso', ast.SetExclude(['100', '200'], approximate=True))]))
-
- # one value
- self.assertEqual(ast_from_string("mime : text"),
- ast.AND([ast.Token('mime', ast.SetInclude(['text']))]))
- self.assertEqual(ast_from_string("mime in text"),
- ast.AND([ast.Token('mime', ast.SetInclude(['text']))]))
- self.assertEqual(ast_from_string("mime = text"),
- ast.AND([ast.Token('mime', ast.SetInclude(['text']))]))
- self.assertEqual(ast_from_string("mime ~ text"),
- ast.AND([ast.Token('mime', ast.SetInclude(['text'], approximate=True))]))
- self.assertEqual(ast_from_string("mime != text"),
- ast.AND([ast.Token('mime', ast.SetExclude(['text']))]))
- self.assertEqual(ast_from_string("mime not in text"),
- ast.AND([ast.Token('mime', ast.SetExclude(['text']))]))
- self.assertEqual(ast_from_string("mime !~ text"),
- ast.AND([ast.Token('mime', ast.SetExclude(['text'], approximate=True))]))
-
- # expressions with slash and comma
- self.assertEqual(ast_from_string('mime : "text"'),
- ast.AND([ast.Token('mime', ast.SetInclude(['text']))]))
- self.assertEqual(ast_from_string('mime : "text", "plain"'),
- ast.AND([ast.Token('mime', ast.SetInclude(['text', 'plain']))]))
- self.assertEqual(ast_from_string('mime : "text, plain"'),
- ast.AND([ast.Token('mime', ast.SetInclude(['text, plain']))]))
- self.assertEqual(ast_from_string('mime ~ "text/plain"'),
- ast.AND([ast.Token('mime', ast.SetInclude(['text/plain'], approximate=True))]))
- self.assertEqual(ast_from_string('mime = ("text/plain", "image/jpeg")'),
- ast.AND([ast.Token('mime', ast.SetInclude(['text/plain', 'image/jpeg']))]))
-
- def test_parse_tag(self):
-
- # only tag: tag, tags, (tag), (tags)
- self.assertEqual(ast_from_string("foo"),
- ast.AND([ast.Token('tag', ast.SetInclude(['foo']))]))
- self.assertEqual(ast_from_string("(foo)"),
- ast.AND([ast.Token('tag', ast.SetInclude(['foo']))]))
- self.assertEqual(ast_from_string("foo, bar"),
- ast.AND([ast.Token('tag', ast.SetInclude(['foo', 'bar']))]))
- self.assertEqual(ast_from_string("foo,bar"),
- ast.AND([ast.Token('tag', ast.SetInclude(['foo', 'bar']))]))
- self.assertEqual(ast_from_string("(foo, bar,foobar)"),
- ast.AND([ast.Token('tag', ast.SetInclude(['foo', 'bar', 'foobar']))]))
-
- # op and tag: !tag, ~tag, !~tag
- self.assertEqual(ast_from_string("~foo"),
- ast.AND([ast.Token('tag', ast.SetInclude(['foo'], approximate=True))]))
- self.assertEqual(ast_from_string("~ foo"),
- ast.AND([ast.Token('tag', ast.SetInclude(['foo'], approximate=True))]))
- self.assertEqual(ast_from_string("!foo"),
- ast.AND([ast.Token('tag', ast.SetExclude(['foo']))]))
- self.assertEqual(ast_from_string("! foo"),
- ast.AND([ast.Token('tag', ast.SetExclude(['foo']))]))
- self.assertEqual(ast_from_string("!~foo"),
- ast.AND([ast.Token('tag', ast.SetExclude(['foo'], approximate=True))]))
- self.assertEqual(ast_from_string("!~ foo"),
- ast.AND([ast.Token('tag', ast.SetExclude(['foo'], approximate=True))]))
-
- # op and list: ! (tags), ~tags, ...
- self.assertEqual(ast_from_string("~ foo, bar"),
- ast.AND([ast.Token('tag', ast.SetInclude(['foo', 'bar'], approximate=True))]))
- self.assertEqual(ast_from_string("~foo, bar"),
- ast.AND([ast.Token('tag', ast.SetInclude(['foo', 'bar'], approximate=True))]))
- self.assertEqual(ast_from_string("~ (foo, bar)"),
- ast.AND([ast.Token('tag', ast.SetInclude(['foo', 'bar'], approximate=True))]))
- self.assertEqual(ast_from_string("! foo, bar"),
- ast.AND([ast.Token('tag', ast.SetExclude(['foo', 'bar']))]))
- self.assertEqual(ast_from_string("! (foo, bar)"),
- ast.AND([ast.Token('tag', ast.SetExclude(['foo', 'bar']))]))
- self.assertEqual(ast_from_string("! (foo,bar)"),
- ast.AND([ast.Token('tag', ast.SetExclude(['foo', 'bar']))]))
- self.assertEqual(ast_from_string("!~ foo, bar"),
- ast.AND([ast.Token('tag', ast.SetExclude(['foo', 'bar'], approximate=True))]))
- self.assertEqual(ast_from_string("!~ (foo, bar)"),
- ast.AND([ast.Token('tag', ast.SetExclude(['foo', 'bar'], approximate=True))]))
- self.assertEqual(ast_from_string("!~(foo,bar)"),
- ast.AND([ast.Token('tag', ast.SetExclude(['foo', 'bar'], approximate=True))]))
-
- def test_parse_query(self):
- # simple query
- self.assertEqual(ast_from_string('foo / bar'), ast.AND([
- ast.Token('tag', ast.SetInclude('foo')),
- ast.Token('tag', ast.SetInclude('bar'))]))
- self.assertEqual(ast_from_string('iso in ("foo", "bar") / mime = plain'), ast.AND([
- ast.Token('iso', ast.SetInclude('foo', 'bar')),
- ast.Token('mime', ast.SetInclude('plain'))]))
- self.assertEqual(ast_from_string('iso in ("foo", "bar") / mime = plain'), ast.AND([
- ast.Token('iso', ast.SetInclude('foo', 'bar')),
- ast.Token('mime', ast.SetInclude('plain'))]))
- self.assertEqual(ast_from_string('iso = 1.23 / rank < 5'), ast.AND([
- ast.Token('iso', ast.Continuous(1.23, 1.23, True, True)),
- ast.Token('rank', ast.Continuous(hi=5))]))
- self.assertEqual(ast_from_string('time >= 12:50 / time < 13:50'), ast.AND([
- ast.Token('time', ast.TimeRange(lo=datetime(1970, 1, 1, 12, 50), lo_inc=True, hi_inc=True)),
- ast.Token('time', ast.TimeRange(hi=datetime(1970, 1, 1, 13, 50), lo_inc=True, hi_inc=False))]))
- self.assertEqual(ast_from_string('time >= 17.5.2001 / time < 18.4.2002'), ast.AND([
- ast.Token('time', ast.Datetime(lo=datetime(2001, 5, 17, 0, 0), lo_inc=True)),
- ast.Token('time', ast.Datetime(hi=datetime(2002, 4, 18, 0, 0)))]))
- # mixing expressions
- self.assertEqual(ast_from_string('foo / iso in "bar" / mime ~ "text/plain" / iso < 100 / time >= 17.5.2001 / time < 13:50'), ast.AND([
- ast.Token('tag', ast.SetInclude('foo')),
- ast.Token('iso', ast.SetInclude('bar')),
- ast.Token('mime', ast.SetInclude('text/plain', approximate=True)),
- ast.Token('iso', ast.Continuous(hi=100)),
- ast.Token('time', ast.Datetime(lo=datetime(2001, 5, 17, 0, 0), lo_inc=True)),
- ast.Token('time', ast.TimeRange(hi=datetime(1970, 1, 1, 13, 50), lo_inc=True))]))
-
- # leading/trailing slashes
- self.assertRaises(errors.ParserError, ast_from_string, '/ foobar')
- self.assertRaises(errors.ParserError, ast_from_string, 'foobar /')
- self.assertRaises(errors.ParserError, ast_from_string, 'foobar / ')
- self.assertRaises(errors.ParserError, ast_from_string, 'foo // bar')
- self.assertRaises(errors.ParserError, ast_from_string, 'foo / / bar')
-
- def test_quoting(self):
- self.assertEqual(ast_from_string("tag in ('(foo, bar)', foobar)"),
- ast.AND([ast.Token('tag', ast.SetInclude(['(foo, bar)', 'foobar']))]))
- self.assertEqual(ast_from_string('tag in ("(foo, bar)", foobar)'),
- ast.AND([ast.Token('tag', ast.SetInclude(['(foo, bar)', 'foobar']))]))
- self.assertEqual(ast_from_string('tag in ("(foo, \\"bar\\")", foobar)'),
- ast.AND([ast.Token('tag', ast.SetInclude(['(foo, "bar")', 'foobar']))]))
- self.assertEqual(ast_from_string('tag in ("(foo, bar)", "foobar")'),
- ast.AND([ast.Token('tag', ast.SetInclude(['(foo, bar)', 'foobar']))]))
- self.assertEqual(ast_from_string('tag in ("(foo, bar)", \'foobar\')'),
- ast.AND([ast.Token('tag', ast.SetInclude(['(foo, bar)', 'foobar']))]))
-
- # error cases
- self.assertRaises(errors.ParserError, ast_from_string, ('tag in ("(foo, bar, foobar)'))
- self.assertRaises(errors.ParserError, ast_from_string, ("tag in ('(foo, bar, foobar)"))
-
-
-## main ##
-
-if __name__ == '__main__':
- unittest.main()
-
-## EOF ##