aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2023-03-05 19:17:00 +0100
committerMatthias Baumgartner <dev@igsor.net>2023-03-05 19:17:00 +0100
commit5a325565f917c8b1d233d8e6373756c253400909 (patch)
treee6e0b475c7ab5c6a7ff4f0ea7ad1b08cecf05e68 /test
parente1e77797454ac747b293f589d8f2e0243173a419 (diff)
parent98e567933723c59d1d97b3a85e649cfdce514676 (diff)
downloadtagit-0.23.03.tar.gz
tagit-0.23.03.tar.bz2
tagit-0.23.03.zip
Merge branch 'develop'v0.23.03
Diffstat (limited to 'test')
-rw-r--r--test/__init__.py0
-rw-r--r--test/config/__init__.py0
-rw-r--r--test/config/test_schema.py284
-rw-r--r--test/config/test_settings.py903
-rw-r--r--test/config/test_types.py251
-rw-r--r--test/parsing/__init__.py0
-rw-r--r--test/parsing/filter/__init__.py0
-rw-r--r--test/parsing/filter/test_from_string.py751
-rw-r--r--test/parsing/filter/test_to_string.py111
-rw-r--r--test/parsing/test_datefmt.py378
-rw-r--r--test/parsing/test_sort.py96
-rw-r--r--test/utils/__init__.py0
-rw-r--r--test/utils/test_builder.py173
-rw-r--r--test/utils/test_frame.py168
-rw-r--r--test/utils/test_time.py159
15 files changed, 3274 insertions, 0 deletions
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/__init__.py
diff --git a/test/config/__init__.py b/test/config/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/config/__init__.py
diff --git a/test/config/test_schema.py b/test/config/test_schema.py
new file mode 100644
index 0000000..9e3d3b7
--- /dev/null
+++ b/test/config/test_schema.py
@@ -0,0 +1,284 @@
+"""
+
+Part of the tagit test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import unittest
+
+# tagit imports
+from tagit.config import types
+from tagit.utils import errors
+
+# objects to test
+from tagit.config.schema import ConfigSchema, ConfigKey, ConfigTitle, IncompatibleTypes
+
+
+## code ##
+
+def _config_title_check(self, key0, key1):
+ self.assertEqual(key0, key1)
+ self.assertEqual(key0._title, key1._title)
+ self.assertEqual(key0._description, key1._description)
+ self.assertSetEqual(key0._modules, key1._modules)
+
+def _config_key_check(self, key0, key1):
+ self.assertEqual(key0, key1)
+ self.assertEqual(key0._title, key1._title)
+ self.assertEqual(key0._description, key1._description)
+ self.assertSetEqual(key0._modules, key1._modules)
+ self.assertSetEqual(key0._examples, key1._examples)
+
+class TestConfigSchema(unittest.TestCase):
+ def test_collection(self):
+ # getitem, call
+ # contains
+ # iter
+ # len
+ # keys
+ pass
+
+ def test_title(self):
+ schema = ConfigSchema()
+ # recognize title
+ schema.declare_title(('some', 'key'), 'module', 'title', 'description')
+ schema.declare(('other', 'key'), types.Int(), 0,
+ 'module', 'title', 'description', 'example')
+ self.assertTrue(schema.is_title(('some', 'key')))
+ self.assertFalse(schema.is_title(('other', 'key')))
+ _config_title_check(self, schema.get_title(('some', 'key')),
+ ConfigTitle(('some', 'key'), 'title', 'module', 'description'))
+ self.assertRaises(KeyError, schema.get_title, ('other', 'key'))
+ # config takes precedence
+ schema.declare(('some', 'key'), types.Int(), 0,
+ 'module', 'title', 'description', 'example')
+ self.assertFalse(schema.is_title(('some', 'key')))
+ # can still retrieve the title
+ _config_title_check(self, schema.get_title(('some', 'key')),
+ ConfigTitle(('some', 'key'), 'title', 'module', 'description'))
+
+ def test_declare(self):
+ schema = ConfigSchema()
+ # keys can be declared
+ schema.declare(('some', 'key'), types.Int(), 0,
+ 'module', 'title', 'description', 'example')
+ self.assertSetEqual(set(schema.config.keys()), {('some', 'key')})
+ _config_key_check(self, schema.config[('some', 'key')],
+ ConfigKey(('some', 'key'), types.Int(), 0,
+ 'module', 'title', 'description', 'example'))
+ # double insert with identical signature is accepted
+ schema.declare(('some', 'key'), types.Int(), 0,
+ 'module', 'title', 'description', 'example')
+ self.assertSetEqual(set(schema.config.keys()), {('some', 'key')})
+ _config_key_check(self, schema.config[('some', 'key')],
+ ConfigKey(('some', 'key'), types.Int(), 0,
+ 'module', 'title', 'description', 'example'))
+ # additional info is accepted
+ schema.declare(('some', 'key'), types.Int(), 0,
+ 'other_module', 'other_title', 'other_description', 'other_example')
+ self.assertSetEqual(set(schema.config.keys()), {('some', 'key')})
+ ck = ConfigKey(('some', 'key'), types.Int(), 0,
+ 'module', 'other_title', 'other_description', 'example')
+ ck._examples.add('other_example')
+ ck._modules.add('other_module')
+ _config_key_check(self, schema.config[('some', 'key')], ck)
+
+ # empty key is rejected
+ self.assertRaises(errors.ProgrammingError, schema.declare, [], types.Int(), 0)
+ # empty type is rejected
+ self.assertRaises(AttributeError, schema.declare, ('foo', ), None, None)
+ # invalid defaults are rejected
+ self.assertRaises(errors.ProgrammingError, schema.declare, ('foo', ), types.Unsigned(), -1)
+ self.assertRaises(errors.ProgrammingError, schema.declare, ('foo', ), types.Int(), 'abc')
+ self.assertRaises(errors.ProgrammingError, schema.declare, ('foo', ), types.String(), 123)
+ self.assertRaises(errors.ProgrammingError, schema.declare, ('foo', ), types.Enum('foo'), 'bar')
+ # double insert with different signature is rejected
+ self.assertRaises(IncompatibleTypes, schema.declare, ('some', 'key'), types.Unsigned(), 0)
+ self.assertRaises(IncompatibleTypes, schema.declare, ('some', 'key'), types.Int(), 2)
+
+ def test_declare_title(self):
+ schema = ConfigSchema()
+ # titles can be declared
+ schema.declare_title(('some', 'key'), 'module', 'title', 'description')
+ self.assertSetEqual(set(schema.titles.keys()), {('some', 'key')})
+ _config_title_check(self, schema.titles[('some', 'key')],
+ ConfigTitle(('some', 'key'),
+ 'title', 'module', 'description'))
+ # double insert is accepted
+ schema.declare_title(('some', 'key'), 'module', 'title', 'description')
+ self.assertSetEqual(set(schema.titles.keys()), {('some', 'key')})
+ _config_title_check(self, schema.titles[('some', 'key')], ConfigTitle(('some', 'key'),
+ 'title', 'module', 'description'))
+ # additional info is accepted
+ schema.declare_title(('some', 'key'), 'other_module', 'other_title', 'other_description')
+ self.assertSetEqual(set(schema.titles.keys()), {('some', 'key')})
+ ck = ConfigTitle(('some', 'key'), 'other_title', 'module', 'other_description')
+ ck._modules.add('other_module')
+ _config_title_check(self, schema.titles[('some', 'key')], ck)
+ # empty key is rejected
+ self.assertRaises(errors.ProgrammingError, schema.declare_title, [], types.Int(), 0)
+ # title and config key can exist in parallel
+ schema.declare(('other', 'key'), types.Int(), 0)
+ self.assertSetEqual(set(schema.config.keys()), {('other', 'key')})
+ _config_key_check(self, schema.config[('other', 'key')],
+ ConfigKey(('other', 'key'), types.Int(), 0))
+ schema.declare_title(('other', 'key'), 'module', 'title', 'description')
+ self.assertIn(('other', 'key'), set(schema.titles.keys()))
+ _config_title_check(self, schema.titles[('other', 'key')], ConfigTitle(('other', 'key'),
+ 'title', 'module', 'description'))
+
+
+class TestConfigTitle(unittest.TestCase):
+ def test_magicks(self):
+ ck = ConfigTitle(('some', 'key'), 'title', 'module', 'description')
+ # representation
+ self.assertEqual(repr(ck), "ConfigTitle(('some', 'key'), title)")
+ # comparison
+ self.assertEqual(ck, ConfigTitle(('some', 'key')))
+ self.assertEqual(hash(ck), hash(ConfigTitle(('some', 'key'))))
+ self.assertNotEqual(ck, ConfigTitle(('other', 'key')))
+
+ def test_properties(self):
+ ck = ConfigTitle(('some', 'key'), 'title', 'module', 'some_description')
+ # key can't be overwritten
+ self.assertRaises(AttributeError, ck.__setattr__, 'key', ('other', 'key'))
+ # modules
+ ck.modules = 'other_module'
+ self.assertSetEqual(ck.modules, {'module', 'other_module'})
+ ck.modules = None
+ self.assertSetEqual(ck.modules, {'module', 'other_module'})
+ ck.modules = ''
+ self.assertSetEqual(ck.modules, {'module', 'other_module'})
+ # title
+ ck.title = 'other_title'
+ self.assertEqual(ck.title, 'other_title')
+ ck.title = None
+ self.assertEqual(ck.title, 'other_title')
+ ck.title = ''
+ self.assertEqual(ck.title, 'other_title')
+ # description
+ ck.description = 'other_description'
+ self.assertEqual(ck.description, 'other_description')
+ ck.description = None
+ self.assertEqual(ck.description, 'other_description')
+ ck.description = ''
+ self.assertEqual(ck.description, 'other_description')
+
+ def test_tree(self):
+ self.assertEqual(ConfigTitle(('some', 'path', 'to', 'a', 'key')).branch,
+ ('some', 'path', 'to', 'a'))
+ self.assertEqual(ConfigTitle(('some', 'path', 'to', 'a', 'key')).leaf,
+ 'key')
+ self.assertEqual(ConfigTitle(('somekey', )).branch,
+ tuple())
+ self.assertEqual(ConfigTitle(('somekey', )).leaf,
+ 'somekey')
+
+
+class TestConfigKey(unittest.TestCase):
+ def test_magicks(self):
+ ck = ConfigKey(('some', 'key'), types.Int(), 0,
+ 'module', 'title', 'some_description', 'some_example')
+ ck.example = 'other_example'
+ ck.modules = 'other_module'
+ # representation
+ self.assertEqual(repr(ck), "ConfigKey(('some', 'key'), Int, 0)")
+ # comparison
+ self.assertEqual(ck, ConfigKey(('some', 'key'), types.Int(), 0))
+ self.assertEqual(hash(ck), hash(ConfigKey(('some', 'key'), types.Int(), 0)))
+ self.assertNotEqual(ck, ConfigKey(('some', 'key'), types.Int(), 1))
+ self.assertNotEqual(ck, ConfigKey(('some', 'key'), types.Unsigned(), 0))
+ self.assertNotEqual(ck, ConfigKey(('other', 'key'), types.Int(), 0))
+
+ def test_properties(self):
+ ck = ConfigKey(('some', 'key'), types.Int(), 0,
+ 'module', 'title', 'some_description', 'some_example')
+ self.assertEqual(ck.key, ('some', 'key'))
+ self.assertEqual(ck.type, types.Int())
+ self.assertEqual(ck.default, 0)
+ self.assertSetEqual(ck.modules, {'module'})
+ self.assertEqual(ck.title, 'title')
+ self.assertEqual(ck.description, 'some_description')
+ self.assertEqual(ck.example, 'some_example')
+
+ # key, type, default can't be overwritten
+ self.assertRaises(AttributeError, ck.__setattr__, 'key', ('other', 'key'))
+ self.assertRaises(AttributeError, ck.__setattr__, 'type', types.Unsigned())
+ self.assertRaises(AttributeError, ck.__setattr__, 'default', 123)
+ # modules
+ ck.modules = 'other_module'
+ self.assertSetEqual(ck.modules, {'module', 'other_module'})
+ ck.modules = None
+ self.assertSetEqual(ck.modules, {'module', 'other_module'})
+ ck.modules = ''
+ self.assertSetEqual(ck.modules, {'module', 'other_module'})
+ # title
+ ck.title = 'other_title'
+ self.assertEqual(ck.title, 'other_title')
+ ck.title = None
+ self.assertEqual(ck.title, 'other_title')
+ ck.title = ''
+ self.assertEqual(ck.title, 'other_title')
+ # description
+ ck.description = 'other_description'
+ self.assertEqual(ck.description, 'other_description')
+ ck.description = None
+ self.assertEqual(ck.description, 'other_description')
+ ck.description = ''
+ self.assertEqual(ck.description, 'other_description')
+ # example
+ ck.example = 'other_example'
+ ex = ck.example
+ self.assertTrue('other_example' in ex)
+ self.assertTrue('some_example' in ex)
+ ck.example = None
+ self.assertEqual(ck.example, ex)
+ ck.example = ''
+ self.assertEqual(ck.example, ex)
+ # type example if unspecified
+ self.assertEqual(ConfigKey(('some', 'key'), types.Int(), 0).example, types.Int().example)
+
+ def test_checks(self):
+ # check
+ self.assertTrue(ConfigKey(('some', 'key'), types.Int(), 0).check(0))
+ self.assertFalse(ConfigKey(('some', 'key'), types.Int(), 0).check(1.23))
+ self.assertFalse(ConfigKey(('some', 'key'), types.Int(), 0).check('foobar'))
+ self.assertTrue(ConfigKey(('some', 'key'), types.Unsigned(), 0).check(0))
+ self.assertFalse(ConfigKey(('some', 'key'), types.Unsigned(), 0).check(-1))
+ self.assertTrue(ConfigKey(('some', 'key'), types.String(), 0).check('foobar'))
+ self.assertFalse(ConfigKey(('some', 'key'), types.String(), 0).check(['foo', 'bar']))
+
+ # backtrack
+ self.assertTrue(ConfigKey(['somekey'], types.Int(), 0).backtrack(0))
+ self.assertTrue(ConfigKey(['somekey'], types.Int(), 0).backtrack(-1))
+ self.assertTrue(ConfigKey(['somekey'], types.Int(), 0).backtrack(123))
+ self.assertRaises(types.ConfigTypeError,
+ ConfigKey(['somekey'], types.Int(), 0).backtrack, 1.23)
+ self.assertRaises(types.ConfigTypeError,
+ ConfigKey(['somekey'], types.Int(), 0).backtrack, 'foobar')
+ self.assertTrue(ConfigKey(['somekey'], types.Unsigned(), 0).backtrack(0))
+ self.assertRaises(types.ConfigTypeError,
+ ConfigKey(['somekey'], types.Unsigned(), 0).backtrack, -1)
+ self.assertTrue(ConfigKey(['somekey'], types.String(), 0).backtrack('foobar'))
+ self.assertRaises(types.ConfigTypeError,
+ ConfigKey(['somekey'], types.String(), 0).backtrack, ['foo', 'bar'])
+
+ def test_tree(self):
+ self.assertEqual(ConfigKey(('some', 'path', 'to', 'a', 'key'), types.Int(), 0).branch,
+ ('some', 'path', 'to', 'a'))
+ self.assertEqual(ConfigKey(('some', 'path', 'to', 'a', 'key'), types.Int(), 0).leaf,
+ 'key')
+ self.assertEqual(ConfigKey(('somekey', ), types.Int(), 0).branch,
+ tuple())
+ self.assertEqual(ConfigKey(('somekey', ), types.Int(), 0).leaf,
+ 'somekey')
+
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/config/test_settings.py b/test/config/test_settings.py
new file mode 100644
index 0000000..d7ce7f8
--- /dev/null
+++ b/test/config/test_settings.py
@@ -0,0 +1,903 @@
+"""Test settings loading and access.
+
+Part of the tagit test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import io
+import json
+import os
+import tempfile
+import unittest
+
+# tagit imports
+from tagit.config import Settings, ConfigError, types
+
+# objects to test
+from tagit.config.schema import ConfigSchema
+
+
+## code ##
+
+class TestSettings(unittest.TestCase):
+ def setUp(self):
+ # example schema
+ self.schema = ConfigSchema()
+ self.schema.declare(('ui', 'standalone', 'browser', 'cols'), types.Unsigned(), 3)
+ self.schema.declare(('session', 'paths', 'library'), types.Path(), '')
+ self.schema.declare(('session', 'paths', 'config'), types.Path(), '')
+ self.schema.declare(('extraction', 'constant', 'enabled'), types.Bool(), False)
+
+ # example config
+ self.cfg = Settings(schema=self.schema, data={
+ ('session', 'paths', 'library'): '/path/to/lib',
+ ('session', 'messaging', 'verbose'): 8,
+ })
+
+ def test_retrieval(self):
+ # get configured
+ self.assertEqual('/path/to/lib', self.cfg('session', 'paths', 'library'))
+ # get default
+ self.assertEqual(3, self.cfg('ui', 'standalone', 'browser', 'cols'))
+ # get invalid
+ self.assertRaises(KeyError, self.cfg, 'ui', 'standalone', 'browser', 'rows')
+ # get unknown
+ self.assertEqual(8, self.cfg('session', 'messaging', 'verbose'))
+ self.assertRaises(KeyError, self.cfg, 'session', 'messaging', 'debug')
+ # get with default
+ self.assertEqual('/path/to/lib', self.cfg('session', 'paths', 'library', default='foo'))
+ self.assertEqual(8, self.cfg('session', 'messaging', 'verbose', default=12))
+ self.assertEqual(5, self.cfg('ui', 'standalone', 'browser', 'cols', default=5))
+ self.assertEqual(3.14, self.cfg('ui', 'standalone', 'browser', 'rows', default=3.14))
+ # get if invalid was configured
+ cfg = Settings(schema=self.schema, data={
+ ('session', 'paths', 'library'): 123,
+ ('session', 'messaging', 'verbose'): 8,
+ })
+ self.assertEqual(8, cfg('session', 'messaging', 'verbose'))
+ self.assertEqual('foobar', cfg('session', 'paths', 'library', default='foobar'))
+ self.assertEqual('', cfg('session', 'paths', 'library'))
+ # test aliases
+ self.assertEqual('/path/to/lib', self.cfg['session', 'paths', 'library'])
+ self.assertEqual('/path/to/lib', self.cfg[('session', 'paths', 'library')])
+
+ def test_view(self):
+ # retrieve view
+ sub = self.cfg('ui', 'standalone', 'browser')
+ self.assertListEqual(list(sub), [
+ ('cols', )])
+ # set in original affects view
+ self.assertEqual(3, sub('cols'))
+ self.cfg.set(('ui', 'standalone', 'browser', 'cols'), 4)
+ self.assertEqual(4, sub('cols'))
+ # unset in original affects view
+ self.cfg.unset('ui', 'standalone', 'browser', 'cols')
+ self.assertEqual(3, sub('cols'))
+ # set in view affects original
+ sub.set(('cols', ), 5)
+ self.assertEqual(5, sub('cols'))
+ self.assertEqual(5, self.cfg('ui', 'standalone', 'browser', 'cols'))
+ # unset in view affects original
+ sub.unset('cols')
+ self.assertEqual(3, sub('cols'))
+ self.assertEqual(3, self.cfg('ui', 'standalone', 'browser', 'cols'))
+ # has
+ self.assertIn(('cols', ), sub)
+ self.assertNotIn(('rows', ), sub)
+ self.assertNotIn(('ui', 'standalone', 'browser'), sub)
+ # unspecified view
+ sub = self.cfg('session', 'messaging')
+ self.assertListEqual(list(sub), [
+ ('verbose', )])
+ # upper branch view
+ sub = self.cfg('session')
+ self.assertCountEqual(list(sub), [
+ ('paths', 'library'),
+ ('paths', 'config'),
+ ('messaging', 'verbose')])
+ self.assertEqual('/path/to/lib', sub('paths', 'library'))
+ self.assertEqual(8, sub('messaging', 'verbose'))
+ self.assertRaises(KeyError, sub, 'session', 'paths', 'library')
+ # defaults
+ sub = self.cfg('ui', 'standalone')
+ self.assertCountEqual(list(sub), [
+ ('browser', 'cols')])
+ # aliases
+ sub = self.cfg['session']
+ self.assertCountEqual(list(sub), [
+ ('paths', 'library'),
+ ('paths', 'config'),
+ ('messaging', 'verbose')
+ ])
+
+ def test_modify_value(self):
+ # knowns
+ # overwrite default
+ self.cfg.set(('ui', 'standalone', 'browser', 'cols'), 4)
+ self.assertEqual(4, self.cfg('ui', 'standalone', 'browser', 'cols'))
+ # overwrite configured
+ self.cfg.set(('ui', 'standalone', 'browser', 'cols'), 5)
+ self.assertEqual(5, self.cfg('ui', 'standalone', 'browser', 'cols'))
+ # remove configured
+ self.cfg.unset('ui', 'standalone', 'browser', 'cols')
+ self.assertEqual(3, self.cfg('ui', 'standalone', 'browser', 'cols'))
+ # remove default
+ self.cfg.unset('ui', 'standalone', 'browser', 'cols')
+ self.assertEqual(3, self.cfg('ui', 'standalone', 'browser', 'cols'))
+ # overwrite with invalid
+ self.assertRaises(TypeError, self.cfg.set, ('ui', 'standalone', 'browser', 'cols'), 'hello')
+ # set to default
+ self.cfg.set(('session', 'paths', 'library'), '')
+ self.assertEqual('', self.cfg('session', 'paths', 'library'))
+ self.assertNotIn(('session', 'paths', 'library'), self.cfg.config)
+
+ # unknowns
+ # overwrite unknown
+ self.cfg.set(('session', 'messaging', 'verbose'), 1)
+ self.assertEqual(1, self.cfg('session', 'messaging', 'verbose'))
+ # overwrite unknonw with different type
+ self.cfg.set(('session', 'messaging', 'verbose'), 'hello')
+ self.assertEqual('hello', self.cfg('session', 'messaging', 'verbose'))
+ # remove unknown
+ self.cfg.unset('session', 'messaging', 'verbose')
+ self.assertRaises(KeyError, self.cfg, 'session', 'messaging', 'verbose')
+
+ # define new unknown
+ self.cfg.set(('storage', 'library', 'autosave'), 15)
+ self.assertEqual(15, self.cfg('storage', 'library', 'autosave'))
+ # overwrite new unknown
+ self.cfg.set(('storage', 'library', 'autosave'), 5)
+ self.assertEqual(5, self.cfg('storage', 'library', 'autosave'))
+ # overwrite new unknown with different type
+ self.cfg.set(('storage', 'library', 'autosave'), 'hello')
+ self.assertEqual('hello', self.cfg('storage', 'library', 'autosave'))
+ # remove unknown
+ self.cfg.unset('storage', 'library', 'autosave')
+ self.assertRaises(KeyError, self.cfg, 'storage', 'library', 'autosave')
+ # remove invalid
+ self.cfg.unset('storage', 'library', 'autosave')
+ self.assertRaises(KeyError, self.cfg, 'storage', 'library', 'autosave')
+
+ # alias
+ self.cfg['storage', 'library', 'autosave'] = 12
+ self.assertEqual(12, self.cfg('storage', 'library', 'autosave'))
+ del self.cfg['storage', 'library', 'autosave']
+ self.assertRaises(KeyError, self.cfg, 'storage', 'library', 'autosave')
+
+ def test_set_branch(self):
+ # knowns
+ cfg = Settings(schema=self.schema)
+ cfg.set(('session', ), {
+ 'paths': {'library': '/path/to/other'},
+ 'messaging': {'debug': True}})
+ self.assertDictEqual(cfg.config, {
+ ('session', 'paths', 'library'): '/path/to/other',
+ ('session', 'messaging', 'debug'): True,
+ })
+ cfg.set(('session', 'paths'), {
+ 'library': '',
+ 'config': '/path/to/config',
+ 'numerical': '/path/to/num',
+ })
+ self.assertDictEqual(cfg.config, {
+ # library is omitted because it's the default
+ ('session', 'paths', 'config'): '/path/to/config',
+ ('session', 'paths', 'numerical'): '/path/to/num',
+ ('session', 'messaging', 'debug'): True, # keey rest
+ })
+ # error: value
+ self.assertRaises(TypeError, cfg.set, ('session', ), {
+ 'paths': {'library': 1234},
+ 'messaging': {'debug': True}
+ })
+ # error: subkey
+ self.assertRaises(TypeError, cfg.set, ('session', ), {
+ 'paths': 123,
+ 'messaging': {'debug': True}
+ })
+ # error: superkey
+ self.assertRaises(TypeError, cfg.set, ('session', ), {
+ 'paths': {'library': {'path': '/path/to/lib'}},
+ 'messaging': {'debug': True}
+ })
+
+ # unknowns
+ cfg = Settings(schema=self.schema)
+ cfg.set(('storage', ), {
+ 'library': {'autosave': 10, 'write_through': True},
+ 'index': {'preview_size': [10, 20, 30]}})
+ self.assertDictEqual(cfg.config, {
+ ('storage', 'library', 'autosave'): 10,
+ ('storage', 'library', 'write_through'): True,
+ ('storage', 'index', 'preview_size'): [10, 20, 30],
+ })
+ # unknowns with previous keys
+ cfg.set(('storage', ), {
+ 'library': {'autoindex': 20, 'autosave': 20},
+ 'numerical': {'write_through': False}})
+ self.assertDictEqual(cfg.config, {
+ ('storage', 'library', 'autosave'): 20, # change value
+ ('storage', 'library', 'autoindex'): 20, # add key
+ ('storage', 'library', 'write_through'): True, # keep key
+ ('storage', 'numerical', 'write_through'): False, # add key
+ ('storage', 'index', 'preview_size'): [10, 20, 30], # keep key
+ })
+ # error: superkey
+ self.assertRaises(TypeError, cfg.set, ('storage', ), {
+ 'library': {'autoindex': {'autosave': 20}},
+ 'numerical': {'write_through': True}})
+ # error: subkey
+ self.assertRaises(TypeError, cfg.set, ('storage', ), {
+ 'library': 123,
+ 'numerical': {'write_through': True}})
+
+ # alias
+ cfg['session'] = {
+ 'paths': {'library': '', 'numerical': 'otherpath'},
+ 'messaging': {'verbose': 3, 'debug': False}
+ }
+ self.assertDictEqual(cfg.config, {
+ ('session', 'paths', 'numerical'): 'otherpath',
+ ('session', 'messaging', 'verbose'): 3,
+ ('session', 'messaging', 'debug'): False,
+ # keep the rest as-is
+ ('storage', 'library', 'autosave'): 20, # change value
+ ('storage', 'library', 'autoindex'): 20, # add key
+ ('storage', 'library', 'write_through'): True, # keep key
+ ('storage', 'numerical', 'write_through'): False, # add key
+ ('storage', 'index', 'preview_size'): [10, 20, 30], # keep key
+ })
+ cfg[('session', )] = {
+ 'paths': {'library': '/path/to/somewhere', 'numerical': 'mypath'},
+ 'messaging': {'debug': True}
+ }
+ self.assertDictEqual(cfg.config, {
+ ('session', 'paths', 'library'): '/path/to/somewhere',
+ ('session', 'paths', 'numerical'): 'mypath',
+ ('session', 'messaging', 'debug'): True,
+ # keep the rest as-is
+ ('session', 'messaging', 'verbose'): 3,
+ ('storage', 'library', 'autosave'): 20, # change value
+ ('storage', 'library', 'autoindex'): 20, # add key
+ ('storage', 'library', 'write_through'): True, # keep key
+ ('storage', 'numerical', 'write_through'): False, # add key
+ ('storage', 'index', 'preview_size'): [10, 20, 30], # keep key
+ })
+
+ # test nesting
+ cfg['session']['paths'] = {'library': '/path/to/elsewhere'}
+ self.assertEqual('/path/to/elsewhere', cfg('session', 'paths', 'library'))
+ cfg['session', 'paths'] = {'library': '/path/to/elsewhere2'}
+ self.assertEqual('/path/to/elsewhere2', cfg('session', 'paths', 'library'))
+ cfg['session']['paths']['library'] = '/path/to/elsewhere3'
+ self.assertEqual('/path/to/elsewhere3', cfg('session', 'paths', 'library'))
+
+ def test_set_branch_dict(self):
+ # unknowns
+ cfg = Settings(schema=ConfigSchema(), data={
+ ('session', 'paths', 'preview', 'files'): 'thumbs'
+ })
+ # adding siblings is allowed
+ cfg.set(('session', 'paths', 'preview', 'original'), '')
+ self.assertDictEqual(cfg.config, {
+ ('session', 'paths', 'preview', 'files'): 'thumbs',
+ ('session', 'paths', 'preview', 'original'): '',
+ })
+ # clearing sub-items is allowed
+ cfg.set(('session', 'paths', 'preview'), {})
+ self.assertDictEqual(cfg.config, {
+ ('session', 'paths', 'preview'): {},
+ })
+ # adding sub-items is allowed
+ cfg.set(('session', 'paths', 'preview', 'sqlite'), 'thumbs.db')
+ self.assertDictEqual(cfg.config, {
+ ('session', 'paths', 'preview', 'sqlite'): 'thumbs.db',
+ })
+ # adding siblings dict-style is allowed
+ cfg.set(('session', 'paths', 'preview'), {'exif': 'none', 'rest': {}})
+ self.assertDictEqual(cfg.config, {
+ ('session', 'paths', 'preview', 'sqlite'): 'thumbs.db',
+ ('session', 'paths', 'preview', 'exif'): 'none',
+ ('session', 'paths', 'preview', 'rest'): {},
+ })
+ # adding sub-items to non-dict items is not allowed
+ self.assertRaises(TypeError, cfg.set, ('session', 'paths', 'preview', 'exif', 'sub'), 123)
+ # adding super-items of non-dict type is not allowed
+ self.assertRaises(TypeError, cfg.set, ('session', 'paths', 'preview'), 123)
+
+ def test_unset_branch(self):
+ # mixed knowns/unknowns
+ self.cfg.unset('session')
+ self.assertNotIn(('session', 'messaging', 'verbose'), self.cfg)
+ self.assertNotIn(('session', 'messaging'), self.cfg)
+ self.assertIn(('session', ), self.cfg)
+ self.assertEqual('', self.cfg('session', 'paths', 'library'))
+
+ # already empty
+ self.cfg.unset('ui', 'standalone')
+ self.assertEqual(3, self.cfg('ui', 'standalone', 'browser', 'cols'))
+
+ def test_unset_branch_alias(self):
+ # mixed knowns/unknowns
+ del self.cfg['session']
+ self.assertNotIn(('session', 'messaging', 'verbose'), self.cfg)
+ self.assertNotIn(('session', 'messaging'), self.cfg)
+ self.assertIn(('session', ), self.cfg)
+ self.assertEqual('', self.cfg('session', 'paths', 'library'))
+
+ # already empty
+ del self.cfg['ui', 'standalone']
+ self.assertEqual(3, self.cfg('ui', 'standalone', 'browser', 'cols'))
+
+ def test_has(self):
+ # check default
+ self.assertIn(('ui', 'standalone', 'browser', 'cols'), self.cfg)
+ # check configured
+ self.assertIn(('session', 'paths', 'library'), self.cfg)
+ # check newly configured
+ self.cfg.set(('ui', 'standalone', 'browser', 'cols'), 4)
+ self.assertIn(('ui', 'standalone', 'browser', 'cols'), self.cfg)
+ # check invalid
+ self.assertNotIn(('ui', 'standalone', 'browser', 'rows'), self.cfg)
+ # check branch
+ self.assertIn(('ui', 'standalone', 'browser'), self.cfg)
+ # check branch
+ self.assertNotIn(('ui', 'standalone', 'filter'), self.cfg)
+
+ # check unknown
+ self.assertIn(('session', 'messaging', 'verbose'), self.cfg)
+ self.assertNotIn(('session', 'messaging', 'debug'), self.cfg)
+ # check unknown branch
+ self.assertIn(('session', 'messaging'), self.cfg)
+ self.assertNotIn(('storage', 'library'), self.cfg)
+ self.assertNotIn(('storage', ), self.cfg)
+
+ def test_iteration(self):
+ # length
+ self.assertEqual(5, len(self.cfg))
+ # iterate keys, explicit/implicit
+ self.assertCountEqual(list(self.cfg), [
+ ('ui', 'standalone', 'browser', 'cols'),
+ ('extraction', 'constant', 'enabled'),
+ ('session', 'paths', 'library'),
+ ('session', 'paths', 'config'),
+ ('session', 'messaging', 'verbose')])
+ self.assertCountEqual(list(self.cfg.keys()), [
+ ('ui', 'standalone', 'browser', 'cols'),
+ ('extraction', 'constant', 'enabled'),
+ ('session', 'paths', 'library'),
+ ('session', 'paths', 'config'),
+ ('session', 'messaging', 'verbose')])
+ # iterate items
+ self.assertCountEqual(list(self.cfg.items()), [
+ (('ui', 'standalone', 'browser', 'cols'), 3),
+ (('extraction', 'constant', 'enabled'), False),
+ (('session', 'paths', 'library'), '/path/to/lib'),
+ (('session', 'paths', 'config'), ''),
+ (('session', 'messaging', 'verbose'), 8)])
+
+ def test_magicks(self):
+ # comparison
+ self.assertEqual(self.cfg, self.cfg)
+ self.assertEqual(self.cfg, self.cfg())
+ self.assertNotEqual(self.cfg, self.cfg('ui'))
+ self.assertEqual(self.cfg, Settings(self.schema, data=self.cfg.config))
+ self.assertNotEqual(self.cfg, Settings(ConfigSchema(), data=self.cfg.config))
+ self.assertNotEqual(self.cfg, Settings(self.schema))
+ self.assertEqual(hash(self.cfg), hash(self.cfg))
+ self.assertEqual(hash(self.cfg), hash(self.cfg()))
+ self.assertEqual(hash(self.cfg), hash(Settings(self.schema, data=self.cfg.config)))
+ # representation
+ self.assertEqual(str(self.cfg), str(self.cfg.config))
+ self.assertEqual(repr(self.cfg), 'Settings(prefix=(), keys=2, len=5)')
+
+ def test_diff(self):
+ """
+ # diff
+ >>> cfg.unset('ui', 'standalone', 'browser', 'cols')
+ >>> cfg.diff(Settings())
+ Settings({'session': {'paths': {'library': '/path/to/lib'}}})
+ >>> cfg.set(('ui', 'standalone', 'browser', 'cols'), 4)
+ >>> cfg.diff(Settings())
+ Settings({'session': {'paths': {'library': '/path/to/lib'}},
+ 'ui': {'standalone': {'browser': {'cols': 4}}}})
+ """
+ # contains all configured
+ self.assertDictEqual(self.cfg.diff(Settings()).config, {
+ ('session', 'paths', 'library'): '/path/to/lib',
+ ('session', 'messaging', 'verbose'): 8,
+ })
+ # still no cols (because it won't be stored in self.cfg)
+ self.cfg.set(('ui', 'standalone', 'browser', 'cols'), 3)
+ self.assertDictEqual(self.cfg.diff(Settings()).config, {
+ ('session', 'paths', 'library'): '/path/to/lib',
+ ('session', 'messaging', 'verbose'): 8,
+ })
+ # still no cols (because it's the default)
+ self.assertDictEqual(self.cfg.diff(Settings(data=
+ {('ui', 'standalone', 'browser', 'cols'): 3}
+ )).config, {
+ ('session', 'paths', 'library'): '/path/to/lib',
+ ('session', 'messaging', 'verbose'): 8,
+ })
+ # now with cols (because it deviates)
+ self.assertDictEqual(self.cfg.diff(Settings(data=
+ {('ui', 'standalone', 'browser', 'cols'): 5}
+ )).config, {
+ ('ui', 'standalone', 'browser', 'cols'): 3,
+ ('session', 'paths', 'library'): '/path/to/lib',
+ ('session', 'messaging', 'verbose'): 8,
+ })
+ # non-differing known
+ self.assertDictEqual(self.cfg.diff(Settings(data=
+ {('session', 'paths', 'library'): '/path/to/lib'}
+ )).config, {
+ ('session', 'messaging', 'verbose'): 8,
+ })
+ # non-differing unknown
+ self.assertDictEqual(self.cfg.diff(Settings(data=
+ {('session', 'messaging', 'verbose'): 8}
+ )).config, {
+ ('session', 'paths', 'library'): '/path/to/lib',
+ })
+
+ def test_flatten_tree(self):
+ schema = ConfigSchema()
+ schema.declare(('ui', 'standalone', 'tiledocks'),
+ types.Dict(types.String(), types.Dict(types.String(), types.Int())), {})
+ schema.declare(('session', 'debug'), types.Bool(), True)
+ schema.declare(('storage', 'library', 'autosave'), types.Unsigned(), 0)
+
+ # ordinary scenario
+ flat = Settings.flatten_tree({
+ 'session': {
+ 'debug': True,
+ 'paths': {
+ 'library': '/path/to/lib',
+ 'config': {'path': '/path/to/conf' },
+ }
+ }}, schema, on_error='raise')
+ self.assertDictEqual(flat, {
+ ('session', 'debug'): True, # defaults are kept
+ ('session', 'paths', 'library'): '/path/to/lib',
+ ('session', 'paths', 'config', 'path'): '/path/to/conf',
+ })
+
+ # dict types
+ flat = Settings.flatten_tree({
+ 'ui': {
+ 'standalone': {
+ 'tiledocks': {
+ 'dashboard': {
+ 'Buttons': 123
+ }}}}}, schema, on_error='raise')
+ self.assertDictEqual(flat, {
+ ('ui', 'standalone', 'tiledocks'): {'dashboard': {'Buttons': 123}}})
+
+ flat = Settings.flatten_tree({
+ 'ui': {
+ 'standalone': {
+ 'tiledocks': {
+ 'dashboard': {}
+ }}}}, schema)
+ self.assertDictEqual(flat, {
+ ('ui', 'standalone', 'tiledocks'): {'dashboard': {}}
+ })
+
+ # dict types w/o schema
+ flat = Settings.flatten_tree({
+ 'ui': {
+ 'standalone': {
+ 'tiledocks': {
+ 'dashboard': {
+ 'Buttons': 123
+ }}}}}, ConfigSchema(), on_error='raise')
+ self.assertDictEqual(flat, {
+ ('ui', 'standalone', 'tiledocks', 'dashboard', 'Buttons'): 123})
+
+ # dict types w/o schema
+ flat = Settings.flatten_tree({
+ 'ui': {
+ 'standalone': {
+ 'tiledocks': {
+ 'dashboard': {
+ 'Buttons': {}
+ }}}}}, ConfigSchema(), on_error='raise')
+ self.assertDictEqual(flat, {
+ ('ui', 'standalone', 'tiledocks', 'dashboard', 'Buttons'): {}})
+
+ # error case: invalid value
+ config = {
+ 'session': {
+ 'debug': 123,
+ 'paths': {
+ 'library': '/path/to/lib',
+ 'config': {'path': '/path/to/conf' },
+ }
+ }}
+ self.assertRaises(TypeError, Settings.flatten_tree, config, schema, on_error='raise')
+ flat = Settings.flatten_tree(config, schema, on_error='ignore')
+ self.assertDictEqual(flat, {
+ # debug is omitted because it's invalid
+ ('session', 'paths', 'library'): '/path/to/lib',
+ ('session', 'paths', 'config', 'path'): '/path/to/conf',
+ })
+ # error case: invalid subkey
+ config = {
+ 'session': {
+ 'debug': {
+ 'verbose': True
+ },
+ 'paths': {
+ 'library': '/path/to/lib',
+ 'config': {'path': '/path/to/conf' },
+ }
+ }}
+ self.assertRaises(TypeError, Settings.flatten_tree, config, schema, on_error='raise')
+ flat = Settings.flatten_tree(config, schema, on_error='ignore')
+ self.assertDictEqual(flat, {
+ # debug is omitted because it's invalid
+ ('session', 'paths', 'library'): '/path/to/lib',
+ ('session', 'paths', 'config', 'path'): '/path/to/conf',
+ })
+ # error case: invalid superkey
+ config = {
+ 'session': 123,
+ 'storage': {
+ 'library': {
+ 'autosave': -4,
+ 'write_through': True
+ }}}
+ self.assertRaises(TypeError, Settings.flatten_tree, config, schema, on_error='raise')
+ flat = Settings.flatten_tree(config, schema, on_error='ignore')
+ self.assertDictEqual(flat, {('storage', 'library', 'write_through'): True})
+
+ def test_clone(self):
+ cfg = self.cfg.clone()
+ self.assertEqual(self.cfg, cfg)
+ cfg.set(('session', 'paths', 'library'), '/path/to/elsewhere')
+ self.assertEqual('/path/to/elsewhere', cfg('session', 'paths', 'library'))
+ self.assertEqual('/path/to/lib', self.cfg('session', 'paths', 'library'))
+ self.assertNotEqual(self.cfg, cfg)
+
+ def test_file_connected(self):
+ self.assertFalse(self.cfg.file_connected())
+ self.cfg.set(('session', 'paths', 'config'), '/path/to/somewhere')
+ self.assertTrue(self.cfg.file_connected())
+ self.cfg.unset('session', 'paths', 'config')
+ self.assertFalse(self.cfg.file_connected())
+
+ def test_schema_changes(self):
+ schema = ConfigSchema()
+ cfg = Settings.Open({
+ 'ui': {'standalone': {'tiledocks': {'Buttons': {'buttons': [1,2,3]},
+ 'Info': {},
+ }}},
+ 'session': {'paths': {'preview': {'files': 'thumbs'}},
+ 'debug': False,
+ 'size': 'will_become_invalid',
+ },
+ }, schema=schema)
+ self.assertDictEqual(cfg.config, {
+ ('ui', 'standalone', 'tiledocks', 'Buttons', 'buttons'): [1,2,3],
+ ('ui', 'standalone', 'tiledocks', 'Info'): {},
+ ('session', 'paths', 'preview', 'files'): 'thumbs',
+ ('session', 'debug'): False,
+ ('session', 'size'): 'will_become_invalid',
+ })
+
+ schema.declare(('ui', 'standalone', 'tiledocks'),
+ types.Dict(types.String(), types.Dict(types.String(), types.Any())), {})
+ schema.declare(('session', 'paths', 'preview'),
+ types.Dict(types.String(), types.String()), {})
+ schema.declare(('session', 'debug'), types.Bool(), False)
+
+ cfg.rebase(clear_defaults=False)
+ self.assertDictEqual(cfg.config, {
+ ('ui', 'standalone', 'tiledocks'): {'Buttons': {'buttons': [1,2,3]}, 'Info': {}},
+ ('session', 'paths', 'preview'): {'files': 'thumbs'},
+ ('session', 'debug'): False, # is still in here
+ ('session', 'size'): 'will_become_invalid',
+ })
+
+ cfg.rebase()
+ self.assertDictEqual(cfg.config, {
+ ('ui', 'standalone', 'tiledocks'): {'Buttons': {'buttons': [1,2,3]}, 'Info': {}},
+ ('session', 'paths', 'preview'): {'files': 'thumbs'},
+ # now session.debug is gone
+ ('session', 'size'): 'will_become_invalid',
+ })
+
+ # create a schema violation
+ schema.declare(('session', 'size'), types.Int(), 0)
+ # raises an error
+ self.assertRaises(TypeError, cfg.rebase)
+ # ignore the error, size will be removed
+ cfg.rebase(on_error='ignore')
+ self.assertDictEqual(cfg.config, {
+ ('ui', 'standalone', 'tiledocks'): {'Buttons': {'buttons': [1,2,3]}, 'Info': {}},
+ ('session', 'paths', 'preview'): {'files': 'thumbs'},
+ })
+
+ def test_to_tree(self):
+ self.assertDictEqual(self.cfg.to_tree(), {
+ 'session': {'paths': {'library': '/path/to/lib'},
+ 'messaging': {'verbose': 8}},
+ })
+ self.assertDictEqual(self.cfg.to_tree(defaults=True), {
+ 'session': {'paths': {'library': '/path/to/lib',
+ 'config': ''},
+ 'messaging': {'verbose': 8}},
+ 'ui': {'standalone': {'browser': {'cols': 3}}},
+ 'extraction': {'constant': {'enabled': False}}
+ })
+ # corner cases
+ self.assertDictEqual(Settings(schema=self.schema, data={}).to_tree(), {})
+ self.assertDictEqual(Settings(schema=self.schema, data={('session', ): True}).to_tree(),
+ {'session': True})
+ # tree from subkey
+ self.assertDictEqual(self.cfg('session', 'paths').to_tree(), {
+ 'library': '/path/to/lib'})
+ self.assertDictEqual(self.cfg('session', 'paths').to_tree(defaults=True), {
+ 'library': '/path/to/lib',
+ 'config': ''})
+
+ def test_Open_formats(self):
+ config = {
+ 'session': {
+ 'paths': {
+ 'library': '/path/to/somewhere',
+ 'numerical': '/path/to/numerical',
+ },
+ 'messaging': {
+ 'debug': True,
+ 'verbose': False,
+ },
+ },
+ 'extraction': {
+ 'constant': {
+ 'enabled': False
+ },
+ }}
+
+ # store to file
+ path = tempfile.mkstemp(prefix='tagit_')[1]
+ with open(path, 'w') as ofile:
+ json.dump(config, ofile)
+
+ # load from json string
+ cfg = Settings.Open(json.dumps(config), schema=self.schema)
+ # default
+ self.assertEqual(3, cfg('ui', 'standalone', 'browser', 'cols'))
+ self.assertEqual(False, cfg('extraction', 'constant', 'enabled'))
+ # invalid
+ self.assertNotIn(('ui', 'standalone', 'browser', 'rows'), cfg)
+ # known
+ self.assertEqual('/path/to/somewhere', cfg('session', 'paths', 'library'))
+ # unknown
+ self.assertEqual('/path/to/numerical', cfg('session', 'paths', 'numerical'))
+ self.assertTrue(cfg('session', 'messaging', 'debug'))
+ self.assertFalse(cfg('session', 'messaging', 'verbose'))
+ # config dict
+ self.assertDictEqual(cfg.config, {
+ ('session', 'paths', 'library'): '/path/to/somewhere',
+ ('session', 'paths', 'numerical'): '/path/to/numerical',
+ ('session', 'messaging', 'debug'): True,
+ ('session', 'messaging', 'verbose'): False,
+ })
+
+ # load from dict
+ cfg2 = Settings.Open(config, schema=self.schema)
+ self.assertDictEqual(cfg.config, cfg2.config)
+
+ # load from file
+ cfg2 = Settings.Open(path, schema=self.schema)
+ self.assertDictEqual(cfg.config, cfg2.config)
+
+ # load from opened file
+ with open(path) as ifile:
+ cfg2 = Settings.Open(ifile, schema=self.schema)
+ self.assertDictEqual(cfg.config, cfg2.config)
+
+ # invalid type
+ self.assertRaises(TypeError, Settings.Open, 15, schema=self.schema)
+
+ os.unlink(path)
+
+ def test_Open_errors(self):
+ # invalid value
+ config = {'session': {'paths': {'library': 15}}}
+ self.assertRaises(types.ConfigTypeError, Settings.Open, config, schema=self.schema)
+ # too deep
+ config = {'session': {'paths': {'library': {'plain': '/path/to/somewhere'}}}}
+ self.assertRaises(types.ConfigTypeError, Settings.Open, config, schema=self.schema)
+ # too shallow
+ config = {'session': {'paths': 123}}
+ self.assertRaises(ConfigError, Settings.Open, config, schema=self.schema)
+ # empty
+ cfg = Settings.Open({}, schema=self.schema)
+ self.assertDictEqual(cfg.config, {})
+
+ def test_Open_dict_keys(self):
+ schema = ConfigSchema()
+ schema.declare(('ui', 'standalone', 'tiledocks'),
+ types.Dict(types.String(), types.Dict(types.String(), types.Any())), {})
+ # correct settings
+ config = {
+ 'ui': {
+ 'standalone': {
+ 'tiledocks': {
+ 'dashboard': {
+ 'Buttons': {
+ 'buttons': ['ShowBrowsing', 'ShowDashboard']
+ },
+ 'Info': {},
+ },
+ 'sidebar': {
+ 'LibSummary': {'size': 200},
+ },
+ }}}}
+ cfg = Settings.Open(config, schema=schema)
+ self.assertDictEqual(cfg('ui', 'standalone', 'tiledocks'),
+ config['ui']['standalone']['tiledocks'])
+ self.assertNotIn(('ui', 'standalone', 'tiledocks', 'dashboard'), cfg)
+
+ # incorrect settings
+ config = {
+ 'ui': {
+ 'standalone': {
+ 'tiledocks': {
+ 'dashboard': {
+ 'Buttons': {
+ 'buttons': ['ShowBrowsing', 'ShowDashboard']
+ },
+ 'Info': {},
+ },
+ 'sidebar': [ 'LibSummary' ],
+ }}}}
+ self.assertRaises(types.ConfigTypeError, Settings.Open, config, schema=schema)
+
+ def test_Open_sequence(self):
+ # start with empty config
+ schema = ConfigSchema()
+ schema.declare(('session', 'paths', 'library'), types.Path(), '')
+ schema.declare(('session', 'verbose'), types.Int(), 0)
+ schema.declare(('session', 'debug'), types.Bool(), False)
+
+ cfg = Settings(schema=schema)
+ # update from first source
+ cfg.update(Settings.Open({
+ 'session': {'verbose': 1}
+ }, schema=schema, clear_defaults=False))
+ # update from second source
+ cfg.update(Settings.Open({
+ 'session': {'paths': {'library': 'foobar'},
+ 'debug': True}
+ }, schema=schema, clear_defaults=False))
+ # update from third source
+ cfg.update(Settings.Open({
+ 'session': {'paths': {'library': 'barfoo'},
+ 'debug': False}
+ }, schema=schema, clear_defaults=False))
+ # check: the default value was cleared
+ self.assertDictEqual(cfg.config, {
+ ('session', 'paths', 'library'): 'barfoo',
+ ('session', 'verbose'): 1,
+ })
+ # update from fourth source
+ cfg.update(Settings.Open({
+ 'session': {'paths': {'library': '/path/to/lib'},
+ 'verbose': 0}
+ }, schema=schema, clear_defaults=False))
+ # check: the default value is again cleared
+ self.assertDictEqual(cfg.config, {
+ ('session', 'paths', 'library'): '/path/to/lib',
+ })
+
+ def test_update(self):
+ # plain test
+ self.cfg.update(Settings(schema=self.schema, data={
+ ('session', 'paths', 'library'): '/path/to/elsewhere',
+ ('session', 'paths', 'config'): '/path/to/somewhere',
+ ('ui', 'standalone', 'browser', 'cols'): 3,
+ ('ui', 'standalone', 'browser', 'rows'): 5,
+ }))
+ self.assertDictEqual(self.cfg.config, {
+ ('session', 'paths', 'library'): '/path/to/elsewhere', # overwrite knowns
+ ('session', 'paths', 'config'): '/path/to/somewhere', # add new knowns
+ # don't add defaults
+ ('ui', 'standalone', 'browser', 'rows'): 5, # add new unknowns
+ ('session', 'messaging', 'verbose'): 8, # keep old values
+ })
+ # reset to default
+ self.cfg.update(Settings(schema=self.schema, data={
+ ('session', 'paths', 'library'): '',
+ ('session', 'messaging', 'verbose'): 5
+ }))
+ self.assertDictEqual(self.cfg.config, {
+ # removed due to becoming default
+ ('session', 'paths', 'config'): '/path/to/somewhere',
+ ('ui', 'standalone', 'browser', 'rows'): 5,
+ ('session', 'messaging', 'verbose'): 5, # overwrite unknowns
+ })
+
+ # error: invalid value
+ self.assertRaises(TypeError, self.cfg.update,
+ Settings(schema=self.schema, data={
+ ('session', 'paths', 'library'): 15,
+ ('ui', 'standalone', 'browser', 'rows'): 5,
+ }))
+
+ # error: known superkey
+ self.assertRaises(TypeError, self.cfg.update,
+ Settings(schema=self.schema, data={
+ ('session', 'paths'): 15,
+ }))
+ # error: known subkey
+ self.assertRaises(TypeError, self.cfg.update,
+ Settings(schema=self.schema, data={
+ ('session', 'paths', 'config', 'write_through'): False,
+ }))
+
+ # error: unknown superkey
+ self.assertRaises(TypeError, self.cfg.update,
+ Settings(schema=self.schema, data={
+ ('session', 'messaging'): 15,
+ ('ui', 'standalone', 'browser', 'rows'): 5,
+ }))
+ # error: unknown subkey
+ self.assertRaises(TypeError, self.cfg.update,
+ Settings(schema=self.schema, data={
+ ('session', 'messaging', 'verbose', 'debug'): True,
+ ('ui', 'standalone', 'browser', 'rows'): 5,
+ }))
+
+ # error: unconfigured known superkey
+ self.assertRaises(TypeError, Settings(schema=self.schema, data={
+ }).update, Settings(schema=ConfigSchema(), data={
+ ('ui', 'standalone', 'browser'): 15
+ }))
+ # error: unconfigured known subkey
+ self.assertRaises(TypeError, Settings(schema=self.schema, data={
+ }).update, Settings(schema=ConfigSchema(), data={
+ ('ui', 'standalone', 'browser', 'cols', 'foo'): 15
+ }))
+
+ # corner case: overwrite unknown subkey with dict
+ cfg = Settings(schema=ConfigSchema(), data={
+ ('view', 'toolbag', 'right', 'Info'): 3
+ }).update(Settings(schema=ConfigSchema(), data={
+ ('view', 'toolbag', 'right'): {}
+ }))
+ self.assertDictEqual(cfg.config, {
+ ('view', 'toolbag', 'right'): {}
+ })
+
+ def test_save(self):
+ # needs uri
+ self.assertRaises(ValueError, self.cfg.save)
+ # can save if uri is known
+ path = tempfile.mkstemp(prefix='tagit_')[1]
+ self.assertEqual(os.stat(path).st_size, 0)
+ self.cfg.save(path)
+ # file has changed
+ self.assertGreater(os.stat(path).st_size, 0)
+ # can restore from file
+ cfg = Settings.Open(path, schema=self.schema)
+ self.assertEqual(cfg, self.cfg)
+ self.assertDictEqual(cfg.config, self.cfg.config)
+ # can save to buffer
+ buf = io.StringIO()
+ self.cfg.save(buf)
+ with open(path) as ifile:
+ self.assertEqual(buf.getvalue(), ifile.read())
+ os.unlink(path)
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/config/test_types.py b/test/config/test_types.py
new file mode 100644
index 0000000..31537e6
--- /dev/null
+++ b/test/config/test_types.py
@@ -0,0 +1,251 @@
+"""
+
+Part of the tagit test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import unittest
+
+# tagit imports
+from tagit.config.types import ConfigTypeError
+
+# objects to test
+from tagit.config import types
+
+
+## code ##
+
+class TestTypes(unittest.TestCase):
+ def test_any(self):
+ inst = types.Any()
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'Any')
+ self.assertEqual(repr(inst), 'Any()')
+ # comparison (eq, hash)
+ self.assertEqual(types.Any(), types.Any())
+ self.assertEqual(hash(types.Any()), hash(types.Any()))
+ # backtrack
+ self.assertIsNone(inst.backtrack(None, ''))
+ self.assertIsNone(inst.backtrack('foobar', ''))
+ self.assertIsNone(inst.backtrack(123, ''))
+ self.assertIsNone(inst.backtrack([], ''))
+ self.assertIsNone(inst.backtrack(inst, ''))
+
+ def test_bool(self):
+ inst = types.Bool()
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'Bool')
+ self.assertEqual(repr(inst), 'Bool()')
+ # comparison (eq, hash)
+ self.assertEqual(types.Bool(), types.Bool())
+ self.assertEqual(hash(types.Bool()), hash(types.Bool()))
+ # backtrack
+ self.assertIsNone(inst.backtrack(True, ''))
+ self.assertIsNone(inst.backtrack(False, ''))
+ self.assertRaises(ConfigTypeError, inst.backtrack, None, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 123, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 'foo', '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, inst, '')
+
+ def test_keybind(self):
+ inst = types.Keybind()
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'Keybind')
+ self.assertEqual(repr(inst), 'Keybind()')
+ # comparison (eq, hash)
+ self.assertEqual(types.Keybind(), types.Keybind())
+ self.assertEqual(hash(types.Keybind()), hash(types.Keybind()))
+ # backtrack
+ self.assertIsNone(inst.backtrack([], ''))
+ self.assertIsNone(inst.backtrack([(5, ('ctrl', ), ('all', ))], ''))
+ self.assertIsNone(inst.backtrack([('abc', ('rest', ), ('all', ))], ''))
+ self.assertRaises(ConfigTypeError, inst.backtrack, None, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 123, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 'foo', '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, inst, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [1,2,3], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [(1,2,3)], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [(1,(3,),3)], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [(1,3,(3,))], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [(3,(3,),('all',))], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [(3,('foobar',),('all',))], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [(3,('all',),(3,))], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [(3,('all',),('foobar',))], '')
+
+ def test_int(self):
+ inst = types.Int()
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'Int')
+ self.assertEqual(repr(inst), 'Int()')
+ # comparison (eq, hash)
+ self.assertEqual(types.Int(), types.Int())
+ self.assertEqual(hash(types.Int()), hash(types.Int()))
+ # backtrack
+ self.assertIsNone(inst.backtrack(0, ''))
+ self.assertIsNone(inst.backtrack(1, ''))
+ self.assertIsNone(inst.backtrack(-1, ''))
+ self.assertRaises(ConfigTypeError, inst.backtrack, None, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 1.23, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 'foo', '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, inst, '')
+
+ def test_unsigned(self):
+ inst = types.Unsigned()
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'Unsigned int')
+ self.assertEqual(repr(inst), 'Unsigned()')
+ # comparison (eq, hash)
+ self.assertEqual(types.Unsigned(), types.Unsigned())
+ self.assertEqual(hash(types.Unsigned()), hash(types.Unsigned()))
+ # backtrack
+ self.assertIsNone(inst.backtrack(0, ''))
+ self.assertIsNone(inst.backtrack(1, ''))
+ self.assertIsNone(inst.backtrack(123, ''))
+ self.assertRaises(ConfigTypeError, inst.backtrack, None, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, -1, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 1.23, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 'foo', '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, inst, '')
+
+ def test_float(self):
+ inst = types.Float()
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'Float')
+ self.assertEqual(repr(inst), 'Float()')
+ # comparison (eq, hash)
+ self.assertEqual(types.Float(), types.Float())
+ self.assertEqual(hash(types.Float()), hash(types.Float()))
+ # backtrack
+ self.assertIsNone(inst.backtrack(1.23, ''))
+ self.assertIsNone(inst.backtrack(-1.23, ''))
+ self.assertIsNone(inst.backtrack(1, ''))
+ self.assertIsNone(inst.backtrack(-1, ''))
+ self.assertRaises(ConfigTypeError, inst.backtrack, None, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 'foo', '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, inst, '')
+
+ def test_string(self):
+ inst = types.String()
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'String')
+ self.assertEqual(repr(inst), 'String()')
+ # comparison (eq, hash)
+ self.assertEqual(types.String(), types.String())
+ self.assertEqual(hash(types.String()), hash(types.String()))
+ # backtrack
+ self.assertIsNone(inst.backtrack('', ''))
+ self.assertIsNone(inst.backtrack('foobar', ''))
+ self.assertRaises(ConfigTypeError, inst.backtrack, None, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 123, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 1.23, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, inst, '')
+
+ def test_path(self):
+ inst = types.Path()
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'Path')
+ self.assertEqual(repr(inst), 'Path()')
+ # comparison (eq, hash)
+ self.assertEqual(types.Path(), types.Path())
+ self.assertEqual(hash(types.Path()), hash(types.Path()))
+ # backtrack
+ self.assertIsNone(inst.backtrack('', ''))
+ self.assertIsNone(inst.backtrack('foobar', ''))
+ self.assertRaises(ConfigTypeError, inst.backtrack, None, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 123, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 1.23, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, inst, '')
+
+ def test_enum(self):
+ inst = types.Enum('foo', 'bar', 123)
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'One out of ({})'.format(
+ ', '.join(str(itm) for itm in inst.options)))
+ self.assertEqual(repr(inst), f'Enum([{inst.options}])')
+ # comparison (eq, hash)
+ self.assertEqual(types.Enum(), types.Enum())
+ self.assertEqual(types.Enum('foo', 'bar'), types.Enum(['foo', 'bar']))
+ self.assertEqual(types.Enum(123, 'bar'), types.Enum(['bar', 123]))
+ self.assertEqual(hash(types.Enum('foo')), hash(types.Enum(['foo'])))
+ # backtrack
+ self.assertIsNone(inst.backtrack('foo', ''))
+ self.assertIsNone(inst.backtrack('bar', ''))
+ self.assertIsNone(inst.backtrack(123, ''))
+ self.assertRaises(ConfigTypeError, inst.backtrack, None, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 'foobar', '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 1.23, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, inst, '')
+
+ def test_list(self):
+ inst = types.List(types.Int())
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'List of Int')
+ self.assertEqual(repr(inst), 'List(Int)')
+ # comparison (eq, hash)
+ self.assertEqual(types.List(types.Int()), types.List(types.Int()))
+ self.assertNotEqual(types.List(types.Int()), types.List(types.Unsigned()))
+ self.assertNotEqual(types.List(types.Unsigned()), types.List(types.Int()))
+ self.assertNotEqual(types.List(types.Unsigned()), types.List(types.String()))
+ self.assertNotEqual(types.List(types.Bool()), types.List(types.String()))
+ self.assertEqual(hash(types.List(types.Int())), hash(types.List(types.Int())))
+ self.assertEqual(hash(types.List(types.Unsigned())), hash(types.List(types.Unsigned())))
+ self.assertEqual(hash(types.List(types.String())), hash(types.List(types.String())))
+ # backtrack
+ self.assertIsNone(inst.backtrack([], ''))
+ self.assertIsNone(inst.backtrack([1,2,3], ''))
+ self.assertIsNone(inst.backtrack((1,2,3), ''))
+ self.assertRaises(ConfigTypeError, inst.backtrack, None, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, {1,2,3}, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 'foobar', '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 1.23, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, ['a', 'b', 'c'], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [123, 'b', 'c'], '')
+
+ def test_dict(self):
+ inst = types.Dict(types.String(), types.Int())
+ # representation (str, repr)
+ self.assertEqual(str(inst), 'Dict from String to Int')
+ self.assertEqual(repr(inst), 'Dict(String, Int)')
+ # comparison (eq, hash)
+ self.assertEqual(types.Dict(types.Int(), types.String()),
+ types.Dict(types.Int(), types.String()))
+ self.assertEqual(types.Dict(types.List(types.Int()), types.String()),
+ types.Dict(types.List(types.Int()), types.String()))
+ self.assertNotEqual(types.Dict(types.Int(), types.Unsigned()),
+ types.Dict(types.Unsigned(), types.Int()))
+ self.assertNotEqual(types.Dict(types.Unsigned(), types.String()),
+ types.Dict(types.Int(), types.String()))
+ self.assertNotEqual(types.Dict(types.Unsigned(), types.String()),
+ types.Dict(types.Int(), types.String()))
+ self.assertEqual(hash(types.Dict(types.Int(), types.String())),
+ hash(types.Dict(types.Int(), types.String())))
+ self.assertEqual(hash(types.Dict(types.List(types.Int()), types.String())),
+ hash(types.Dict(types.List(types.Int()), types.String())))
+ # backtrack
+ self.assertIsNone(inst.backtrack({'foo': 2}, ''))
+ self.assertIsNone(inst.backtrack({'foo': 2, 'bar': 3}, ''))
+ self.assertIsNone(inst.backtrack({}, ''))
+ self.assertRaises(ConfigTypeError, inst.backtrack, None, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, {1,2,3}, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 'foobar', '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, 1.23, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, ['a', 'b', 'c'], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, [123, 'b', 'c'], '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, {2: 'foo', 3: 'bar'}, '')
+ self.assertRaises(ConfigTypeError, inst.backtrack, {'foo': 2, 3: 'bar'}, '')
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/parsing/__init__.py b/test/parsing/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/parsing/__init__.py
diff --git a/test/parsing/filter/__init__.py b/test/parsing/filter/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/parsing/filter/__init__.py
diff --git a/test/parsing/filter/test_from_string.py b/test/parsing/filter/test_from_string.py
new file mode 100644
index 0000000..180820a
--- /dev/null
+++ b/test/parsing/filter/test_from_string.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.from_string import FromString
+
+
+## code ##
+
+class TestFromString(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 = FromString(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/filter/test_to_string.py b/test/parsing/filter/test_to_string.py
new file mode 100644
index 0000000..6df7360
--- /dev/null
+++ b/test/parsing/filter/test_to_string.py
@@ -0,0 +1,111 @@
+"""
+
+Part of the tagit test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import contextlib
+import io
+import unittest
+
+# tagit imports
+with contextlib.redirect_stderr(io.StringIO()):
+ from tagit.parsing.filter.from_string import FromString
+ from tagit.utils import bsfs, errors, ns
+ from tagit.utils.bsfs import ast
+
+ # objects to test
+ from tagit.parsing.filter.to_string import ToString
+
+
+## code ##
+
+class TestFromString(unittest.TestCase):
+ def setUp(self):
+ self.schema = bsfs.schema.from_string('''
+ # common external prefixes
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+
+ # common bsfs prefixes
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+
+ # nodes
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Tag rdfs:subClassOf bsfs:Node .
+
+ # literals
+ bsfs:Time rdfs:subClassOf bsfs:Literal .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
+
+ # predicates
+ bse:mime rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bse:iso rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bse:time rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Time;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:rank rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ ''')
+ self.to_string = ToString(self.schema)
+ self.from_string = FromString(self.schema)
+
+ def test_has(self):
+ self.assertEqual('has iso', self.to_string(self.from_string('has iso')))
+ #from debug import debug
+ #debug(locals(), globals())
+
+ def test_categorical(self):
+ self.assertEqual('iso = 100.0', self.to_string(self.from_string('iso = 100')))
+
+ def test_tag(self):
+ self.assertEqual('foobar', self.to_string(self.from_string('foobar')))
+ self.assertEqual('not foobar', self.to_string(self.from_string('not foobar')))
+ self.assertEqual('~ foobar', self.to_string(self.from_string('~ foobar')))
+ self.assertEqual('!~ foobar', self.to_string(self.from_string('!~ foobar')))
+ self.assertIn(self.to_string(self.from_string('foo, bar')), ('"foo", "bar"', '"bar", "foo"'))
+
+ def test_range(self):
+ self.assertEqual('iso < 100.0', self.to_string(self.from_string('iso < 100')))
+ self.assertEqual('iso <= 100.0', self.to_string(self.from_string('iso <= 100')))
+ self.assertEqual('iso > 100.0', self.to_string(self.from_string('iso > 100')))
+ self.assertEqual('iso >= 100.0', self.to_string(self.from_string('iso >= 100')))
+ self.assertEqual('iso = [10.0 - 100.0]', self.to_string(self.from_string('iso = [10-100]')))
+ raise NotImplementedError() # FIXME: test with dates!
+
+ def test_entity(self):
+ self.assertEqual('id in "http://example.com/entity#1234"',
+ self.to_string(self.from_string('id in "http://example.com/entity#1234"')))
+
+ def test_group(self):
+ self.assertEqual('group in "http://example.com/group#1234"',
+ self.to_string(self.from_string('group in "http://example.com/group#1234"')))
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/parsing/test_datefmt.py b/test/parsing/test_datefmt.py
new file mode 100644
index 0000000..3f80c15
--- /dev/null
+++ b/test/parsing/test_datefmt.py
@@ -0,0 +1,378 @@
+"""Test datetime parser.
+
+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 date as ddate
+from datetime import time as dtime
+from datetime import datetime
+
+# external imports
+from pyparsing import ParseException
+
+# tagit imports
+from tagit.utils import errors, Struct
+
+# objects to test
+#from tagit.parsing.datefmt import DatefmtError, DateParserError, TimeParserError, DateFormatError, guess_date, guess_time, guess_datetime, increment, PRIORITIES_US, DF
+from tagit.parsing.datefmt import guess_date, guess_time, PRIORITIES_US, DateParserError, DateFormatError, TimeParserError, guess_datetime, DF, parse_datetime, increment, DateTimeParser
+
+
+## code ##
+
+class TestGuessDatetime(unittest.TestCase):
+ def test_parse_datetime(self):
+ parse_datetime = DateTimeParser()
+ cyear = ddate.today().year
+ cmon = ddate.today().month
+ cday = ddate.today().day
+
+ # date only: vary number formats
+ self.assertEqual(parse_datetime('3.4.12'), datetime(2012, 4, 3))
+ self.assertEqual(parse_datetime('15.8.19'), datetime(2019, 8, 15))
+ self.assertEqual(parse_datetime('8.11.98'), datetime(1998, 11, 8))
+ self.assertEqual(parse_datetime('10.11.12'), datetime(2012, 11, 10))
+ self.assertEqual(parse_datetime('3.4.1912'), datetime(1912, 4, 3))
+ self.assertEqual(parse_datetime('15.8.1919'), datetime(1919, 8, 15))
+ self.assertEqual(parse_datetime('8.11.1998'), datetime(1998, 11, 8))
+ self.assertEqual(parse_datetime('10.11.1912'), datetime(1912, 11, 10))
+ self.assertEqual(parse_datetime('8.1998'), datetime(1998, 8, 1))
+ self.assertEqual(parse_datetime('10.1912'), datetime(1912, 10, 1))
+ self.assertRaises(errors.ParserError, parse_datetime, 'ab.cd.ef')
+ self.assertRaises(errors.ParserError, parse_datetime, '123.123.2000')
+ self.assertRaises(errors.ParserError, parse_datetime, '.123.2000')
+ self.assertRaises(errors.ParserError, parse_datetime, '123..2000')
+ self.assertRaises(errors.ParserError, parse_datetime, '12.12.20001')
+ # date only: vary order (three-part)
+ self.assertEqual(parse_datetime('15.98.8'), datetime(1998, 8, 15))
+ self.assertEqual(parse_datetime('8.98.15'), datetime(1998, 8, 15))
+ self.assertEqual(parse_datetime('8.15.98'), datetime(1998, 8, 15))
+ self.assertEqual(parse_datetime('15.8.98'), datetime(1998, 8, 15))
+ self.assertEqual(parse_datetime('98.8.15'), datetime(1998, 8, 15))
+ self.assertEqual(parse_datetime('98.15.8'), datetime(1998, 8, 15))
+ # date only: vary order (two-part)
+ self.assertEqual(parse_datetime('15.98'), datetime(1998, 1, 15))
+ self.assertEqual(parse_datetime('08.98'), datetime(1998, 8, 1))
+ self.assertEqual(parse_datetime('08.15'), datetime(2015, 8, 1))
+ self.assertEqual(parse_datetime('15.08'), datetime(cyear, 8, 15))
+ self.assertEqual(parse_datetime('98.08'), datetime(1998, 8, 1))
+ self.assertEqual(parse_datetime('98.15'), datetime(1998, 1, 15))
+ # date only: one part
+ self.assertEqual(parse_datetime('1998'), datetime(1998, 1, 1))
+ # date only: literal month
+ self.assertEqual(parse_datetime('98.April'), datetime(1998, 4, 1))
+ # FIXME: Allow more patterns
+ #self.assertEqual(parse_datetime('98, April'), datetime(1998, 4, 1))
+ #self.assertEqual(parse_datetime('April, 15'), datetime(2015, 4, 1))
+ # date only: day with suffix
+ #self.assertEqual(parse_datetime('10th 2000'), datetime(2000, 1, 10))
+ #self.assertEqual(parse_datetime('2000, 10th'), datetime(2000, 1, 10))
+ #self.assertEqual(parse_datetime('April, 10th'), datetime(cyear, 4, 10))
+ #self.assertEqual(parse_datetime('April 10th'), datetime(cyear, 4, 10))
+ #self.assertEqual(parse_datetime('10th April'), datetime(cyear, 4, 10))
+ # date only: of notation
+ #self.assertEqual(parse_datetime('10th of April, 2000'), datetime(2000, 4, 10))
+ #self.assertEqual(parse_datetime('10th of April'), datetime(cyear, 4, 10))
+ #self.assertEqual(parse_datetime('10 of 04'), datetime(cyear, 4, 10))
+ # invalid ranges
+ self.assertRaises(DateParserError, parse_datetime, '10.93.2013')
+ self.assertRaises(DateParserError, parse_datetime, '48.10.2013')
+ self.assertRaises(DateParserError, parse_datetime, '48.93.2013')
+ self.assertRaises(DateParserError, parse_datetime, "52.74")
+
+ # time only: am/pm
+ self.assertEqual(parse_datetime("10 am"), datetime(1970, 1, 1, 10))
+ self.assertEqual(parse_datetime("10 pm"), datetime(1970, 1, 1, 22))
+ self.assertEqual(parse_datetime("10:02 pm"), datetime(1970, 1, 1, 22, 2))
+ self.assertEqual(parse_datetime("14 am"), datetime(1970, 1, 1, 14))
+ self.assertRaises(TimeParserError, parse_datetime, "14 pm")
+ # time only: 24hrs format
+ self.assertEqual(parse_datetime("1:2"), datetime(1970, 1, 1, 1, 2))
+ self.assertEqual(parse_datetime("12:34"), datetime(1970, 1, 1, 12, 34))
+ self.assertEqual(parse_datetime("12:34:54"), datetime(1970, 1, 1, 12, 34, 54))
+ self.assertEqual(parse_datetime("12:34:54.123"), datetime(1970, 1, 1, 12, 34, 54, 123000))
+ self.assertEqual(parse_datetime("1:2:3.4"), datetime(1970, 1, 1, 1, 2, 3, 400000))
+ self.assertRaises(errors.ParserError, parse_datetime, '84:12')
+ self.assertRaises(errors.ParserError, parse_datetime, '12:75')
+ self.assertRaises(errors.ParserError, parse_datetime, '12:13:84')
+ # time only: HH:MM
+ self.assertEqual(parse_datetime("54:34"), datetime(1970, 1, 1, 0, 54, 34))
+ # time only: invalid format
+ self.assertRaises(errors.ParserError, parse_datetime, '12:')
+
+ # date and time
+ self.assertEqual(parse_datetime("12:34 18.05.2012"), datetime(2012, 5, 18, 12, 34))
+ self.assertEqual(parse_datetime("12:34, 18.05.2012"), datetime(2012, 5, 18, 12, 34))
+ self.assertEqual(parse_datetime("18.05.2012 12:34"), datetime(2012, 5, 18, 12, 34))
+ self.assertEqual(parse_datetime("18.05.2012, 12:34"), datetime(2012, 5, 18, 12, 34))
+ self.assertEqual(parse_datetime("2012, 12:34"), datetime(2012, 1, 1, 12, 34))
+ self.assertEqual(parse_datetime("2012, 12am"), datetime(2012, 1, 1, 12))
+ self.assertRaises(errors.ParserError, parse_datetime, '12.34 18:05:2012')
+
+ # invalid args
+ self.assertRaises(errors.ParserError, parse_datetime, '')
+
+ def test_guess_date(self):
+ this_year = ddate.today().year
+ # some unambiguous formats
+ self.assertEqual(guess_date('18 . 05 . 2012'.split()), (ddate(2012, 5, 18), 'DMY'))
+ self.assertEqual(guess_date('18 5 2012'.split()), (ddate(2012, 5, 18), 'DMY'))
+ self.assertEqual(guess_date('2012 , 05 , 18'.split()), (ddate(2012, 5, 18), 'YMD'))
+ self.assertEqual(guess_date('2012 5 18'.split()), (ddate(2012, 5, 18), 'YMD'))
+ self.assertEqual(guess_date('18 5 2004'.split()), (ddate(2004, 5, 18), 'DMY'))
+ self.assertEqual(guess_date('2004 5 18'.split()), (ddate(2004, 5, 18), 'YMD'))
+ self.assertEqual(guess_date('10 11 12'.split()), (ddate(2012, 11, 10), 'DMY'))
+ self.assertEqual(guess_date('10 11 12'.split(), priorities=PRIORITIES_US),
+ (ddate(2012, 10, 11), 'MDY'))
+ self.assertEqual(guess_date('2012 04 05'.split()), (ddate(2012, 4, 5), 'YMD'))
+ self.assertEqual(guess_date('2012 4 5'.split()), (ddate(2012, 4, 5), 'YMD'))
+ self.assertEqual(guess_date('2012 May , 4th'.split()), (ddate(2012, 5, 4), 'YMD'))
+ self.assertEqual(guess_date('4 5 2012'.split()), (ddate(2012, 5, 4), 'DMY'))
+ self.assertEqual(guess_date('4th of May 2012'.split()), (ddate(2012, 5, 4), 'DMY'))
+ self.assertEqual(guess_date('2012 4th of May'.split()), (ddate(2012, 5, 4), 'YDM'))
+
+ # three-part format
+ # unambiguous MD ranges, full year
+ self.assertEqual(guess_date('28 11 2018'.split()), (ddate(2018, 11, 28), 'DMY'))
+ self.assertEqual(guess_date('28 2018 11'.split()), (ddate(2018, 11, 28), 'DYM'))
+ self.assertEqual(guess_date('11 28 2018'.split()), (ddate(2018, 11, 28), 'MDY'))
+ self.assertEqual(guess_date('11 2018 28'.split()), (ddate(2018, 11, 28), 'MYD'))
+ self.assertEqual(guess_date('2018 11 28'.split()), (ddate(2018, 11, 28), 'YMD'))
+ self.assertEqual(guess_date('2018 28 11'.split()), (ddate(2018, 11, 28), 'YDM'))
+ # unambiguous MDY ranges
+ self.assertEqual(guess_date('28 11 98'.split()), (ddate(1998, 11, 28), 'DMY'))
+ self.assertEqual(guess_date('28 98 11'.split()), (ddate(1998, 11, 28), 'DYM'))
+ self.assertEqual(guess_date('11 28 98'.split()), (ddate(1998, 11, 28), 'MDY'))
+ self.assertEqual(guess_date('11 98 28'.split()), (ddate(1998, 11, 28), 'MYD'))
+ self.assertEqual(guess_date('98 11 28'.split()), (ddate(1998, 11, 28), 'YMD'))
+ self.assertEqual(guess_date('98 28 11'.split()), (ddate(1998, 11, 28), 'YDM'))
+ # explicit YMD
+ self.assertEqual(guess_date('10th of April 2018'.split()), (ddate(2018, 4, 10), 'DMY'))
+ self.assertEqual(guess_date('April 10th 98'.split()), (ddate(1998, 4, 10), 'MDY'))
+ self.assertEqual(guess_date('98 April 10th'.split()), (ddate(1998, 4, 10), 'YMD'))
+ self.assertEqual(guess_date('2018 10th of April'.split()), (ddate(2018, 4, 10), 'YDM'))
+ self.assertEqual(guess_date('2018 10 of 04'.split()), (ddate(2018, 4, 10), 'YDM'))
+ # explicit MY
+ self.assertEqual(guess_date('2018 10 April'.split()), (ddate(2018, 4, 10), 'YDM'))
+ self.assertEqual(guess_date('2018 April 10'.split()), (ddate(2018, 4, 10), 'YMD'))
+ self.assertEqual(guess_date('April 10 2018'.split()), (ddate(2018, 4, 10), 'MDY'))
+ # explicit DY
+ self.assertEqual(guess_date('10th 04 98'.split()), (ddate(1998, 4, 10), 'DMY'))
+ self.assertEqual(guess_date('2018 10th 04'.split()), (ddate(2018, 4, 10), 'YDM'))
+ self.assertEqual(guess_date('2018 04 10th'.split()), (ddate(2018, 4, 10), 'YMD'))
+ # explicit DM
+ self.assertEqual(guess_date('10th April 10'.split()), (ddate(2010, 4, 10), 'DMY'))
+ self.assertEqual(guess_date('10 10th April'.split()), (ddate(2010, 4, 10), 'YDM'))
+ self.assertEqual(guess_date('10 April 10th'.split()), (ddate(2010, 4, 10), 'YMD'))
+ # ambiguous formats: explicit Y
+ self.assertEqual(guess_date('2018 04 08'.split()), (ddate(2018, 4, 8), 'YMD'))
+ self.assertEqual(guess_date('04 2018 08'.split()), (ddate(2018, 8, 4), 'DYM'))
+ self.assertEqual(guess_date('08 04 2018'.split()), (ddate(2018, 4, 8), 'DMY'))
+ self.assertEqual(guess_date('4 8 11'.split()), (ddate(2011, 8, 4), 'DMY'))
+ # ambiguous formats: explicit D
+ self.assertEqual(guess_date('10 4th 11'.split()), (ddate(2011, 10, 4), 'MDY'))
+ self.assertEqual(guess_date('11 10 4th'.split()), (ddate(2011, 10, 4), 'YMD'))
+ self.assertEqual(guess_date('4th 11 10'.split()), (ddate(2010, 11, 4), 'DMY'))
+ self.assertEqual(guess_date('4th 10 11'.split()), (ddate(2011, 10, 4), 'DMY'))
+ # ambiguous formats: explicit M
+ self.assertEqual(guess_date('April 21 08'.split()), (ddate(2008, 4, 21), 'MDY'))
+ self.assertEqual(guess_date('08 April 21'.split()), (ddate(2021, 4, 8), 'DMY'))
+ self.assertEqual(guess_date('21 08 April'.split()), (ddate(2021, 4, 8), 'YDM'))
+ # fully ambiguous
+ self.assertEqual(guess_date('04 08 10'.split()), (ddate(2010, 8, 4), 'DMY'))
+ # errors
+ self.assertRaises(DateParserError, guess_date, '2012 98 10'.split())
+ self.assertRaises(DateParserError, guess_date, 'April 98 April'.split())
+ self.assertRaises(DateParserError, guess_date, '10th 98 29'.split())
+
+ # two-part format
+ # unambiguous DY ranges
+ self.assertEqual(guess_date('28 98'.split()), (ddate(1998, 1, 28), 'DY'))
+ self.assertEqual(guess_date('98 28'.split()), (ddate(1998, 1, 28), 'YD'))
+ self.assertEqual(guess_date('2010 28'.split()), (ddate(2010, 1, 28), 'YD'))
+ self.assertEqual(guess_date('28 2010'.split()), (ddate(2010, 1, 28), 'DY'))
+ # explicit DY
+ self.assertEqual(guess_date('28th 2010'.split()), (ddate(2010, 1, 28), 'DY'))
+ self.assertEqual(guess_date('2010 28th'.split()), (ddate(2010, 1, 28), 'YD'))
+ # explicit MY
+ self.assertEqual(guess_date('2010 April'.split()), (ddate(2010, 4, 1), 'YM'))
+ self.assertEqual(guess_date('April 2010'.split()), (ddate(2010, 4, 1), 'MY'))
+ # explicit DM
+ self.assertEqual(guess_date('April 10th'.split()), (ddate(this_year, 4, 10), 'MD'))
+ self.assertEqual(guess_date('10th April'.split()), (ddate(this_year, 4, 10), 'DM'))
+ self.assertEqual(guess_date('10th of April'.split()), (ddate(this_year, 4, 10), 'DM'))
+ self.assertEqual(guess_date('10 of 04'.split()), (ddate(this_year, 4, 10), 'DM'))
+ self.assertEqual(guess_date('10th 4'.split()), (ddate(this_year, 4, 10), 'DM'))
+ # explicit Y
+ self.assertEqual(guess_date('2010 04'.split()), (ddate(2010, 4, 1), 'YM'))
+ self.assertEqual(guess_date('04 2010'.split()), (ddate(2010, 4, 1), 'MY'))
+ self.assertEqual(guess_date('04 98'.split()), (ddate(1998, 4, 1), 'MY'))
+ self.assertEqual(guess_date('98 04'.split()), (ddate(1998, 4, 1), 'YM'))
+ # explicit M
+ self.assertEqual(guess_date('April 10'.split()), (ddate(2010, 4, 1), 'MY'))
+ self.assertEqual(guess_date('10 April'.split()), (ddate(this_year, 4, 10), 'DM'))
+ # explicit D
+ self.assertEqual(guess_date('10th 08'.split()), (ddate(this_year, 8, 10), 'DM'))
+ self.assertEqual(guess_date('08 10th'.split()), (ddate(this_year, 8, 10), 'MD'))
+ # some hints
+ self.assertEqual(guess_date('18 5'.split()), (ddate(this_year, 5, 18), 'DM'))
+ self.assertEqual(guess_date('4 8'.split()), (ddate(this_year, 8, 4), 'DM'))
+ # fully ambiguous
+ self.assertEqual(guess_date('08 10'.split()), (ddate(this_year, 10, 8), 'DM'))
+
+ # one-part format
+ # full year
+ self.assertEqual(guess_date('2018'.split()), (ddate(2018, 1, 1), 'Y'))
+ # short year
+ self.assertEqual(guess_date('18'.split()), (ddate(2018, 1, 1), 'Y'))
+ self.assertEqual(guess_date('98'.split()), (ddate(1998, 1, 1), 'Y'))
+ self.assertEqual(guess_date('08'.split()), (ddate(2008, 1, 1), 'Y'))
+ # non-year token
+ self.assertRaises(DateParserError, guess_date, ('1', ))
+ self.assertRaises(DateParserError, guess_date, ('April', ))
+ self.assertRaises(DateParserError, guess_date, ('10th', ))
+
+ # other errors
+ self.assertRaises(DateParserError, guess_date, '')
+ self.assertRaises(DateParserError, guess_date, 'fuuu'.split())
+ self.assertRaises(DateParserError, guess_date, '1 fuuu'.split())
+ self.assertRaises(DateParserError, guess_date, '1 fuuu bar 2'.split())
+ self.assertRaises(DateParserError, guess_date, '1 2 3 4'.split())
+
+ def test_guess_time(self):
+ # single token
+ self.assertEqual(guess_time(['9']), (dtime(hour=9), 'h'))
+ # am/pm notation
+ self.assertEqual(guess_time("9 am".split()), (dtime(hour=9), 'h'))
+ self.assertEqual(guess_time("10 am".split()), (dtime(hour=10), 'h'))
+ self.assertEqual(guess_time("09 am".split()), (dtime(hour=9), 'h'))
+ self.assertEqual(guess_time("9 pm".split()), (dtime(hour=21), 'h'))
+ self.assertEqual(guess_time("10 pm".split()), (dtime(hour=22), 'h'))
+ self.assertEqual(guess_time("09 pm".split()), (dtime(hour=21), 'h'))
+ self.assertEqual(guess_time("10 02 am".split()), (dtime(hour=10, minute=2), 'hm'))
+ self.assertEqual(guess_time("10 02 pm".split()), (dtime(hour=22, minute=2), 'hm'))
+ self.assertEqual(guess_time("14 am".split()), (dtime(hour=14), 'h'))
+ self.assertRaises(TimeParserError, guess_time, "14 pm".split())
+
+ # 24-hrs notation
+ self.assertEqual(guess_time("12 34".split()), (dtime(hour=12, minute=34), 'hm'))
+ self.assertEqual(guess_time("15 32".split()), (dtime(hour=15, minute=32), 'hm'))
+ self.assertEqual(guess_time("12 04".split()), (dtime(hour=12, minute=4), 'hm'))
+ self.assertEqual(guess_time("12 4".split()), (dtime(hour=12, minute=4), 'hm'))
+ # range
+ self.assertEqual(guess_time("12 58".split()), (dtime(hour=12, minute=58), 'hm'))
+ self.assertEqual(guess_time("31 04".split()), (dtime(minute=31, second=4), 'ms'))
+ self.assertEqual(guess_time("31 58".split()), (dtime(minute=31, second=58), 'ms'))
+ # three terms
+ self.assertEqual(guess_time("12 34 54".split()),
+ (dtime(hour=12, minute=34, second=54), 'hms'))
+ # four terms
+ self.assertEqual(guess_time("12 34 54 984".split()),
+ (dtime(hour=12, minute=34, second=54, microsecond=984000), 'hmsn'))
+ # trailing zeros
+ self.assertEqual(guess_time("12 34 54 98400".split()),
+ (dtime(hour=12, minute=34, second=54, microsecond=984000), 'hmsn'))
+ # leading zeros
+ self.assertEqual(guess_time("12 34 54 098400".split()),
+ (dtime(hour=12, minute=34, second=54, microsecond=98400), 'hmsn'))
+
+ # invalid formats
+ self.assertRaises(TimeParserError, guess_time, [])
+ self.assertRaises(TimeParserError, guess_time, ['0', '1', '2', '3', '4'])
+ self.assertRaises(TimeParserError, guess_time, "83 02".split())
+ self.assertRaises(TimeParserError, guess_time, "52 74".split())
+
+ def test_guess_datetime(self):
+ self.assertEqual(guess_datetime(
+ Struct({'date': '18 05 2012'.split(), 'time': '12 34'.split()})),
+ (datetime(2012, 5, 18, 12, 34), 'DMYhm'))
+ self.assertEqual(guess_datetime(Struct({'date': '18 05 2012'.split()})),
+ (datetime(2012, 5, 18), 'DMY'))
+ self.assertEqual(guess_datetime(Struct({'time': '12 34'.split()})),
+ (datetime(1970, 1, 1, 12, 34), 'hm'))
+ self.assertRaises(DateFormatError, guess_datetime, Struct({}))
+
+ def test_DF(self):
+ # msb
+ self.assertRaises(DateFormatError, DF('').msb)
+ self.assertRaises(DateFormatError, DF('abc').msb)
+ self.assertRaises(DateFormatError, DF('ydHSN').msb)
+ self.assertEqual('Y', DF('Yab').msb())
+ self.assertEqual('Y', DF('YDM').msb())
+ self.assertEqual('Y', DF('MdY').msb())
+ self.assertEqual('D', DF('mDn').msb())
+ self.assertEqual('m', DF('mdn').msb())
+ self.assertEqual('n', DF('nab').msb())
+ # lsb
+ self.assertRaises(DateFormatError, DF('').lsb)
+ self.assertRaises(DateFormatError, DF('abc').lsb)
+ self.assertEqual('Y', DF('Yab').lsb())
+ self.assertEqual('D', DF('YDM').lsb())
+ self.assertEqual('M', DF('MdY').lsb())
+ self.assertEqual('n', DF('mDn').lsb())
+ self.assertEqual('n', DF('nab').lsb())
+ # is_time
+ self.assertTrue(DF('mshn').is_time())
+ self.assertTrue(DF('mh').is_time())
+ self.assertTrue(DF('h').is_time())
+ self.assertTrue(DF('m').is_time())
+ self.assertTrue(DF('s').is_time())
+ self.assertTrue(DF('n').is_time())
+ self.assertFalse(DF('').is_time())
+ self.assertFalse(DF('abc').is_time())
+ self.assertFalse(DF('Msnh').is_time())
+ self.assertFalse(DF('Ymsnh').is_time())
+ self.assertFalse(DF('Dmsnh').is_time())
+ self.assertFalse(DF('YDM').is_time())
+ # is_date
+ self.assertTrue(DF('YDM').is_date())
+ self.assertTrue(DF('YM').is_date())
+ self.assertTrue(DF('DM').is_date())
+ self.assertTrue(DF('DY').is_date())
+ self.assertTrue(DF('Y').is_date())
+ self.assertTrue(DF('D').is_date())
+ self.assertTrue(DF('M').is_date())
+ self.assertFalse(DF('').is_date())
+ self.assertFalse(DF('abc').is_date())
+ self.assertFalse(DF('YDMn').is_date())
+ self.assertFalse(DF('YDm').is_date())
+ self.assertFalse(DF('YDh').is_date())
+ self.assertFalse(DF('hmsn').is_date())
+ # valid
+ self.assertTrue(DF('Y').valid())
+ self.assertTrue(DF('YDMhsmn').valid())
+ self.assertTrue(DF('Yabc').valid())
+ self.assertFalse(DF('').valid())
+ self.assertFalse(DF('abc').valid())
+ self.assertFalse(DF('ydHSN').valid())
+
+
+ def test_increment(self):
+ self.assertRaises(DateFormatError, increment, None, '')
+ self.assertEqual(increment(datetime(1970, 1, 1, 0, 0, 0, 10), DF('n')),
+ datetime(1970, 1, 1, 0, 0, 0, 11))
+ self.assertEqual(increment(datetime(1970, 1, 1, 0, 0, 1), DF('s')),
+ datetime(1970, 1, 1, 0, 0, 2))
+ self.assertEqual(increment(datetime(1970, 1, 1, 0, 1), DF('m')),
+ datetime(1970, 1, 1, 0, 2))
+ self.assertEqual(increment(datetime(1970, 1, 1, 1), DF('h')),
+ datetime(1970, 1, 1, 2))
+ self.assertEqual(increment(datetime(1970, 1, 1, 1), DF('h')),
+ datetime(1970, 1, 1, 2))
+ self.assertEqual(increment(datetime(1970, 2, 3, 4, 5, 6), DF('D')),
+ datetime(1970, 2, 4))
+ self.assertEqual(increment(datetime(1970, 2, 3, 4, 5, 6), DF('M')),
+ datetime(1970, 3, 1))
+ self.assertEqual(increment(datetime(1970, 2, 3, 4, 5, 6), DF('Y')),
+ datetime(1971, 1, 1))
+ self.assertRaises(DateFormatError, increment, datetime(1970, 2, 3, 4, 5, 6), DF('abc'))
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/parsing/test_sort.py b/test/parsing/test_sort.py
new file mode 100644
index 0000000..40c9ee1
--- /dev/null
+++ b/test/parsing/test_sort.py
@@ -0,0 +1,96 @@
+"""
+
+Part of the tagit test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import unittest
+
+# tagit imports
+from tagit.utils import errors
+#from tagit.parsing.search import ast, sortkeys # FIXME: mb/port/parsing
+
+# objects to test
+from tagit.parsing.sort import sort_from_string
+
+
+## code ##
+
+class TestParseSort(unittest.TestCase):
+ def setUp(self):
+ sortkeys.expose('iso',
+ TestScope('attribute', 'iso'), 'Numerical')
+ sortkeys.expose('rank',
+ TestScope('attribute', 'rank'), 'Alphabetical', 'Numerical')
+ sortkeys.expose('time',
+ TestScope('attribute', 'time'), 'Numerical')
+ sortkeys.expose('entity',
+ TestScope('property', 'guid'), 'Alphabetical')
+ sortkeys.expose('tag',
+ TestScope('property', 't_image_create_loc'), 'Anchored')
+ sortkeys.expose('mistake',
+ TestScope('property', 't_image_create_loc'))
+
+ def test_parse_sort(self):
+ # simple patterns
+ self.assertEqual(sort_from_string("time"), ast.NumericalSort('time', False))
+ self.assertEqual(sort_from_string("entity"), ast.AlphabeticalSort('entity', False))
+ self.assertEqual(sort_from_string("time asc"), ast.NumericalSort('time', False))
+ self.assertEqual(sort_from_string("time desc"), ast.NumericalSort('time', True))
+ self.assertEqual(sort_from_string("entity desc"), ast.AlphabeticalSort('entity', True))
+ self.assertEqual(sort_from_string("sort by time"), ast.NumericalSort('time', False))
+ self.assertEqual(sort_from_string("sort by time desc"), ast.NumericalSort('time', True))
+ self.assertEqual(sort_from_string("sort by entity desc"),
+ ast.AlphabeticalSort('entity', True))
+ # full pattern
+ self.assertEqual(sort_from_string("sort alphabetically by entity upwards"),
+ ast.AlphabeticalSort('entity', False))
+ self.assertEqual(sort_from_string("sort numerically by time desc"),
+ ast.NumericalSort('time', True))
+ # invalid type
+ self.assertRaises(errors.ParserError, sort_from_string, "sort alphabetically by time desc")
+ self.assertRaises(errors.ParserError, sort_from_string, "sort numerically by entity desc")
+ self.assertRaises(errors.ParserError, sort_from_string, "sort by time similarity to AF39D281CE3")
+ self.assertRaises(errors.ParserError, sort_from_string, "sort alphabetically by tag down,")
+ # ambiguous type
+ self.assertEqual(sort_from_string("sort alphabetically by rank"),
+ ast.AlphabeticalSort('rank', False))
+ self.assertEqual(sort_from_string("sort numerically by rank"),
+ ast.NumericalSort('rank', False))
+ self.assertRaises(errors.ParserError, sort_from_string, "sort by rank")
+ # anchor pattern
+ self.assertEqual(sort_from_string("sort by tag similarity to AF39D281CE3"),
+ ast.AnchoredSort('tag', 'AF39D281CE3', False))
+ self.assertEqual(sort_from_string("tag AF39D281CE3 up"),
+ ast.AnchoredSort('tag', 'AF39D281CE3', False))
+ self.assertEqual(sort_from_string("tag AF39D281CE3 down"),
+ ast.AnchoredSort('tag', 'AF39D281CE3', True))
+ self.assertRaises(errors.ParserError, sort_from_string, "time AF39D281CE3")
+ self.assertRaises(errors.ParserError, sort_from_string, "sort by tag down,")
+ self.assertRaises(errors.ParserError, sort_from_string, "tag XXXXXXXXXXX down,")
+ # compound statements
+ self.assertEqual(sort_from_string("time, iso"),
+ ast.Order(ast.NumericalSort('time'), ast.NumericalSort('iso')))
+ self.assertEqual(sort_from_string("tag AF39D281CE3 down, time up"),
+ ast.Order(ast.AnchoredSort('tag', 'AF39D281CE3', True),
+ ast.NumericalSort('time', False)))
+ self.assertRaises(errors.ParserError, sort_from_string, "tag AF39D281CE3 down,")
+ # empty string
+ self.assertRaises(errors.ParserError, sort_from_string, "")
+ # invalid predicate
+ self.assertRaises(errors.ParserError, sort_from_string, "foobar")
+ # invalid direction
+ self.assertRaises(errors.ParserError, sort_from_string, "sort by entity sideways")
+ # invalid typedef
+ self.assertRaises(errors.ParserError, sort_from_string, "sort by mistake")
+ # missing anchor
+ self.assertRaises(errors.ParserError, sort_from_string, "sort by time similarity to")
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/utils/__init__.py b/test/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/utils/__init__.py
diff --git a/test/utils/test_builder.py b/test/utils/test_builder.py
new file mode 100644
index 0000000..53a22d0
--- /dev/null
+++ b/test/utils/test_builder.py
@@ -0,0 +1,173 @@
+"""
+
+Part of the tagit test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+from functools import partial
+import unittest
+
+# objects to test
+from tagit.utils.builder import BuilderBase, InvalidFactoryName
+
+
+## code ##
+
+class Foo(object):
+ """Description Foo"""
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+ def __eq__(self, other):
+ return self.args == other.args and self.kwargs == other.kwargs
+
+def bar(*args, **kwargs):
+ """Description bar"""
+ return args, kwargs
+
+class BuilderStub(BuilderBase):
+ _factories = {
+ 'foo': Foo,
+ 'bar': bar,
+ }
+
+class TestBuilderBase(unittest.TestCase):
+ def test_magics(self):
+ builder = BuilderStub()
+ # contains
+ self.assertIn('foo', builder)
+ self.assertIn('bar', builder)
+ self.assertNotIn('foobar', builder)
+ # iter
+ self.assertEqual(set(builder), {'foo', 'bar'})
+ self.assertCountEqual(list(builder), ['foo', 'bar'])
+ self.assertEqual(set(BuilderBase()), set())
+ # len
+ self.assertEqual(len(builder), 2)
+ self.assertEqual(len(BuilderBase()), 0)
+ # eq
+ self.assertEqual(BuilderStub(), BuilderStub())
+ self.assertNotEqual(BuilderBase(), BuilderStub())
+ # hash
+ self.assertEqual(hash(BuilderStub()), hash(BuilderStub()))
+ self.assertEqual(hash(BuilderBase()), hash(BuilderBase()))
+
+ def test_get(self):
+ builder = BuilderStub()
+ # get
+ self.assertEqual(builder.get('foo'), Foo)
+ self.assertEqual(builder.get('bar'), bar)
+ self.assertRaises(InvalidFactoryName, builder.get, 'foobar')
+ # getitem
+ self.assertEqual(builder['foo'], Foo)
+ self.assertEqual(builder['bar'], bar)
+ self.assertRaises(InvalidFactoryName, builder.__getitem__, 'foobar')
+
+ def test_keys(self):
+ self.assertEqual(set(BuilderStub().keys()), {'foo', 'bar'})
+ self.assertEqual(set(BuilderBase().keys()), set())
+
+ def test_describe(self):
+ builder = BuilderStub()
+ self.assertEqual(builder.describe('foo'), 'Description Foo')
+ self.assertEqual(builder.describe('bar'), 'Description bar')
+ self.assertRaises(InvalidFactoryName, builder.describe, 'foobar')
+
+ def test_prepare(self):
+ builder = BuilderStub()
+ # empty args
+ # foo
+ part = builder.prepare('foo')
+ self.assertIsInstance(part, partial)
+ self.assertEqual((part.func, part.args, part.keywords), (Foo, (), {}))
+ # bar
+ part = builder.prepare('bar')
+ self.assertIsInstance(part, partial)
+ self.assertEqual((part.func, part.args, part.keywords), (bar, (), {}))
+ # foobar
+ self.assertRaises(InvalidFactoryName, builder.prepare, 'foobar')
+
+ # args
+ # foo
+ part = builder.prepare('foo', 1, 2, 3)
+ self.assertIsInstance(part, partial)
+ self.assertEqual((part.func, part.args, part.keywords), (Foo, (1, 2, 3), {}))
+ # bar
+ part = builder.prepare('bar', 1, 2, 3)
+ self.assertIsInstance(part, partial)
+ self.assertEqual((part.func, part.args, part.keywords), (bar, (1, 2, 3), {}))
+ # foobar
+ self.assertRaises(InvalidFactoryName, builder.prepare, 'foobar', 1, 2, 3)
+
+ # kwargs
+ # foo
+ part = builder.prepare('foo', arg1='hello', arg2='world')
+ self.assertIsInstance(part, partial)
+ self.assertEqual((part.func, part.args, part.keywords),
+ (Foo, (), {'arg1': 'hello', 'arg2': 'world'}))
+ # bar
+ part = builder.prepare('bar', arg1='hello', arg2='world')
+ self.assertIsInstance(part, partial)
+ self.assertEqual((part.func, part.args, part.keywords),
+ (bar, (), {'arg1': 'hello', 'arg2': 'world'}))
+ # foobar
+ self.assertRaises(InvalidFactoryName, builder.prepare, 'foobar',
+ arg1='hello', arg2='world')
+
+ # mixed
+ # foo
+ part = builder.prepare('foo', 1, 2, 3, arg1='hello', arg2='world')
+ self.assertIsInstance(part, partial)
+ self.assertEqual((part.func, part.args, part.keywords),
+ (Foo, (1, 2, 3), {'arg1': 'hello', 'arg2': 'world'}))
+ # bar
+ part = builder.prepare('bar', 1, 2, 3, arg1='hello', arg2='world')
+ self.assertIsInstance(part, partial)
+ self.assertEqual((part.func, part.args, part.keywords),
+ (bar, (1, 2, 3), {'arg1': 'hello', 'arg2': 'world'}))
+ # foobar
+ self.assertRaises(InvalidFactoryName, builder.prepare, 'foobar',
+ 1, 2, 3, arg1='hello', arg2='world')
+
+ def test_build(self):
+ builder = BuilderStub()
+ # empty args
+ self.assertEqual(builder.build('foo'), Foo())
+ self.assertEqual(builder.build('bar'), bar())
+ self.assertRaises(InvalidFactoryName, builder.build, 'foobar')
+ # args
+ self.assertEqual(builder.build('foo', 1, 2, 3), Foo(1, 2, 3))
+ self.assertEqual(builder.build('bar', 1, 2, 3), bar(1, 2, 3))
+ self.assertRaises(InvalidFactoryName, builder.build, 'foobar', 1, 2, 3)
+ # kwargs
+ self.assertEqual(builder.build('foo', arg1='hello', arg2='world'),
+ Foo(arg1='hello', arg2='world'))
+ self.assertEqual(builder.build('bar', arg1='hello', arg2='world'),
+ bar(arg1='hello', arg2='world'))
+ self.assertRaises(InvalidFactoryName, builder.build, 'foobar',
+ arg1='hello', arg2='world')
+ # mixed
+ self.assertEqual(
+ builder.build('foo', 1, 2, 3, arg1='hello', arg2='world'),
+ Foo(1, 2, 3, arg1='hello', arg2='world'))
+ self.assertEqual(
+ builder.build('bar', 1, 2, 3, arg1='hello', arg2='world'),
+ bar(1, 2, 3, arg1='hello', arg2='world'))
+ self.assertRaises(InvalidFactoryName, builder.build, 'foobar',
+ 1, 2, 3, arg1='hello', arg2='world')
+
+ def test_key_from_instance(self):
+ builder = BuilderStub()
+ self.assertEqual(builder.key_from_instance(Foo()), 'foo')
+ self.assertEqual(builder.key_from_instance(bar), 'bar')
+ self.assertRaises(KeyError, builder.key_from_instance, 'foobar')
+ self.assertRaises(KeyError, builder.key_from_instance, BuilderStub())
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/utils/test_frame.py b/test/utils/test_frame.py
new file mode 100644
index 0000000..79bfa3b
--- /dev/null
+++ b/test/utils/test_frame.py
@@ -0,0 +1,168 @@
+"""
+
+Part of the tagit test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import unittest
+
+# objects to test
+from tagit.utils.frame import Frame
+
+
+## code ##
+
+class EntityStub(object):
+ def __init__(self, guid):
+ self.guid = guid
+ def __eq__(self, other):
+ return self.guid == other.guid
+
+class LibraryStub(object):
+ def __init__(self, guids):
+ self.guids = guids
+ def entity(self, guid):
+ if guid in self.guids:
+ return EntityStub(guid)
+ else:
+ raise KeyError(guid)
+
+class TestFrame(unittest.TestCase):
+ def setUp(self):
+ self.ent0 = EntityStub('ent0')
+ self.ent1 = EntityStub('ent1')
+ self.ent2 = EntityStub('ent2')
+ self.ent3 = EntityStub('ent3')
+ self.ent4 = EntityStub('ent4')
+ self.ent5 = EntityStub('ent5')
+ self.lib = LibraryStub(['ent0', 'ent1', 'ent2', 'ent3'])
+
+ def test_properties(self):
+ # plain test
+ frame = Frame(self.ent0, [self.ent1, self.ent2], 123)
+ self.assertEqual(frame, {
+ 'cursor': self.ent0,
+ 'selection': [self.ent1, self.ent2],
+ 'offset': 123,
+ })
+ self.assertEqual(frame.cursor, self.ent0)
+ self.assertEqual(frame.selection, [self.ent1, self.ent2])
+ self.assertEqual(frame.offset, 123)
+
+ # empty selection
+ frame = Frame(self.ent0, [], 123)
+ self.assertEqual(frame, {
+ 'cursor': self.ent0,
+ 'selection': [],
+ 'offset': 123,
+ })
+ self.assertEqual(frame.cursor, self.ent0)
+ self.assertEqual(frame.selection, [])
+ self.assertEqual(frame.offset, 123)
+
+ # no cursor
+ frame = Frame(None, [self.ent0], 123)
+ self.assertEqual(frame, {
+ 'cursor': None,
+ 'selection': [self.ent0],
+ 'offset': 123,
+ })
+ self.assertEqual(frame.cursor, None)
+ self.assertEqual(frame.selection, [self.ent0])
+ self.assertEqual(frame.offset, 123)
+
+ # no selection
+ frame = Frame(self.ent0, None, 123)
+ self.assertEqual(frame, {
+ 'cursor': self.ent0,
+ 'selection': [],
+ 'offset': 123
+ })
+ self.assertEqual(frame.cursor, self.ent0)
+ self.assertEqual(frame.selection, [])
+ self.assertEqual(frame.offset, 123)
+
+ # Not tested: different list-like selection formats (tuple, list)
+ # This seems ok since such formats would be admissible.
+
+ def test_copy(self):
+ frameA = Frame(self.ent0, [self.ent1, self.ent2], 123)
+ self.assertEqual(frameA, {
+ 'cursor': self.ent0,
+ 'selection': [self.ent1, self.ent2],
+ 'offset': 123
+ })
+
+ frameB = frameA.copy()
+ self.assertEqual(frameB, {
+ 'cursor': self.ent0,
+ 'selection': [self.ent1, self.ent2],
+ 'offset': 123
+ })
+
+ # robust against frame changes
+ frameA['cursor'] = self.ent3
+ self.assertEqual(frameB.cursor, self.ent0)
+ frameA['selection'].append(self.ent3)
+ self.assertEqual(frameB.selection, [self.ent1, self.ent2, self.ent3])
+ frameA['selection'] = [self.ent4, self.ent5]
+ self.assertEqual(frameB.selection, [self.ent1, self.ent2, self.ent3])
+ frameA['offset'] = 321
+ self.assertEqual(frameB.offset, 123)
+
+ # ignorant to object changes
+ self.ent0.guid = 'abc'
+ self.assertEqual(frameA.cursor.guid, 'ent3')
+ self.assertEqual(frameB.cursor.guid, 'abc')
+
+ def test_serialization(self):
+ # empty frame
+ frame = Frame()
+ self.assertEqual(frame,
+ Frame.from_serialized(self.lib, frame.serialize(), ignore_errors=False))
+ # with cursor
+ frame = Frame(self.ent1)
+ self.assertEqual(frame,
+ Frame.from_serialized(self.lib, frame.serialize(), ignore_errors=False))
+
+ # with selection
+ frame = Frame(selection=[self.ent0, self.ent3])
+ self.assertEqual(frame,
+ Frame.from_serialized(self.lib, frame.serialize(), ignore_errors=False))
+
+ # with offset
+ frame = Frame(offset=554)
+ self.assertEqual(frame,
+ Frame.from_serialized(self.lib, frame.serialize(), ignore_errors=False))
+
+ # full frame
+ frame = Frame(self.ent1, [self.ent0, self.ent2], 482)
+ self.assertEqual(frame,
+ Frame.from_serialized(self.lib, frame.serialize(), ignore_errors=False))
+
+ # with invalid values
+ frame = Frame(self.ent5, [self.ent0, self.ent2], 482)
+ self.assertRaises(KeyError,
+ Frame.from_serialized, self.lib, frame.serialize(), ignore_errors=False)
+ frame = Frame(self.ent1, [self.ent5, self.ent2], 482)
+ self.assertRaises(KeyError,
+ Frame.from_serialized, self.lib, frame.serialize(), ignore_errors=False)
+
+ # ignoring invalid values
+ frame = Frame(self.ent5, [self.ent0, self.ent2], 482)
+ self.assertEqual(
+ Frame(None, [self.ent0, self.ent2], 482),
+ Frame.from_serialized(self.lib, frame.serialize(), ignore_errors=True))
+ frame = Frame(self.ent1, [self.ent5, self.ent2], 482)
+ self.assertEqual(
+ Frame(self.ent1, [self.ent2], 482),
+ Frame.from_serialized(self.lib, frame.serialize(), ignore_errors=True))
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/utils/test_time.py b/test/utils/test_time.py
new file mode 100644
index 0000000..a4ffeac
--- /dev/null
+++ b/test/utils/test_time.py
@@ -0,0 +1,159 @@
+"""
+
+Part of the tagit test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+from datetime import datetime, timezone
+import math
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+# external imports
+import pyexiv2
+
+# inner-module imports
+from tagit.parsing import parse_datetime
+
+# objects to test
+from tagit.shared import time as ttime
+
+# constants
+sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
+from testdata import IMAGE_VALID
+
+
+## code ##
+
+class TestTTime(unittest.TestCase):
+ def setUp(self):
+ self.img = tempfile.mkstemp(prefix='tagit_')[1]
+ shutil.copy(IMAGE_VALID['path'], self.img)
+ self.empty = tempfile.mkstemp(prefix='tagit_')[1]
+ # ensure correct file date (fixed, in utc)
+ #os.system('touch -d "29 Oct 2015 14:20:56" {}'.format(self.empty))
+ os.utime(self.empty, (1446124856, 1446124856))
+
+ def tearDown(self):
+ if os.path.exists(self.img): os.unlink(self.img)
+ if os.path.exists(self.empty): os.unlink(self.empty)
+
+ def test_conversions(self):
+ """
+ Files, via os.stat (Timestamp in UTC, Local timezone)
+ Images, from Exif (Timestamp in camera local time, No timezone)
+ Images, from Xmp (Timestamp in camera local time, Timezone from Xmp)
+ now (Timestamp in UTC, Local timezone)
+ Database (timestamp in UTC, timestamp in local time)
+ """
+ # prepare
+ stat = os.stat(self.empty)
+ meta = pyexiv2.ImageMetadata(self.img)
+ meta.read()
+ # Create time objects in the proper format
+
+ # Manually defined
+ dt_manual = datetime(2015, 10, 29, 14, 20, 56)
+
+ # os.stat
+ dt_stat = datetime.fromtimestamp(stat.st_mtime)
+
+ # exif
+ dt_exif = meta['Exif.Photo.DateTimeOriginal'].value.replace(tzinfo=ttime.NoTimeZone)
+
+ # xmp
+ dt_xmp = meta['Xmp.exif.DateTimeOriginal'].value
+
+ # user-specified
+ dt_user = parse_datetime("29.10.2015, 14:20:56")
+
+ # now
+ dt_now = datetime.now()
+
+ # UTC offset
+ offset_ref = dt_manual.replace(tzinfo=timezone.utc).timestamp() - dt_manual.timestamp()
+ offset_ref /= 3600
+ offset_now = dt_now.replace(tzinfo=timezone.utc).timestamp() - dt_now.timestamp()
+ offset_now /= 3600
+
+ # Conversions
+ # Comparable local time
+ self.assertEqual(math.floor(ttime.timestamp_loc(dt_manual) % (3600 * 24) / 3600), 14 )
+ self.assertEqual(math.ceil( ttime.timestamp_loc(dt_manual) % (3600 * 24) / 3600), 15 )
+ self.assertEqual(ttime.timestamp_loc(dt_manual) % (3600 * 24), 51656 )
+ self.assertEqual(ttime.timestamp_loc(dt_manual), ttime.timestamp_loc(dt_stat) )
+ self.assertEqual(ttime.timestamp_loc(dt_manual), ttime.timestamp_loc(dt_exif) )
+ self.assertEqual(ttime.timestamp_loc(dt_manual), ttime.timestamp_loc(dt_xmp) )
+ self.assertEqual(ttime.timestamp_loc(dt_manual), ttime.timestamp_loc(dt_user) )
+
+ # UTC offset
+ self.assertEqual(ttime.utcoffset(dt_manual), offset_ref)
+ self.assertEqual(ttime.utcoffset(dt_stat), offset_ref)
+ self.assertEqual(ttime.utcoffset(dt_exif), None)
+ self.assertEqual(ttime.utcoffset(dt_xmp), 11)
+ self.assertEqual(ttime.utcoffset(dt_user), offset_ref)
+ self.assertEqual(ttime.utcoffset(dt_now), offset_now)
+
+ # Comparable UTC
+ self.assertEqual(ttime.timestamp_utc(dt_now),
+ ttime.timestamp_loc(dt_now) - 3600 * offset_now)
+ self.assertEqual(ttime.timestamp_utc(dt_manual),
+ ttime.timestamp_loc(dt_manual) - 3600 * offset_ref )
+ self.assertEqual(ttime.timestamp_utc(dt_stat),
+ ttime.timestamp_loc(dt_stat) - 3600 * offset_ref)
+ self.assertEqual(ttime.timestamp_utc(dt_user),
+ ttime.timestamp_loc(dt_user) - 3600 * offset_ref)
+ self.assertEqual(ttime.timestamp_utc(dt_exif),
+ ttime.timestamp_loc(dt_exif))
+ self.assertEqual(ttime.timestamp_utc(dt_xmp),
+ ttime.timestamp_loc(dt_xmp) - 3600 * ttime.utcoffset(dt_xmp))
+
+ # Conversion back
+ self.assertEqual(ttime.from_timestamp_utc(
+ ttime.timestamp_utc(dt_now)).timestamp(), dt_now.timestamp())
+ self.assertEqual(ttime.from_timestamp_utc(
+ ttime.timestamp_utc(dt_manual)).timestamp(), dt_manual.timestamp())
+ self.assertEqual(ttime.from_timestamp_utc(
+ ttime.timestamp_utc(dt_stat)).timestamp(), dt_stat.timestamp())
+ self.assertEqual(ttime.from_timestamp_utc(
+ ttime.timestamp_utc(dt_user)).timestamp(), dt_user.timestamp())
+ self.assertEqual(ttime.from_timestamp_utc(
+ ttime.timestamp_utc(dt_exif)).timestamp(), dt_exif.timestamp())
+ self.assertEqual(ttime.from_timestamp_utc(
+ ttime.timestamp_utc(dt_xmp)).timestamp(), dt_xmp.timestamp())
+
+ self.assertEqual(ttime.timestamp_loc(ttime.from_timestamp_loc(
+ ttime.timestamp_loc(dt_now))), ttime.timestamp_loc(dt_now))
+ self.assertEqual(ttime.timestamp_loc(ttime.from_timestamp_loc(
+ ttime.timestamp_loc(dt_manual))), ttime.timestamp_loc(dt_manual))
+ self.assertEqual(ttime.timestamp_loc(ttime.from_timestamp_loc(
+ ttime.timestamp_loc(dt_stat))), ttime.timestamp_loc(dt_stat))
+ self.assertEqual(ttime.timestamp_loc(ttime.from_timestamp_loc(
+ ttime.timestamp_loc(dt_user))), ttime.timestamp_loc(dt_user))
+ self.assertEqual(ttime.timestamp_loc(ttime.from_timestamp_loc(
+ ttime.timestamp_loc(dt_exif))), ttime.timestamp_loc(dt_exif))
+ self.assertEqual(ttime.timestamp_loc(ttime.from_timestamp_loc(
+ ttime.timestamp_loc(dt_xmp))), ttime.timestamp_loc(dt_xmp))
+
+ # Conversion to local time + TZO for storage.library
+ # This is used to store file infos
+
+ # Retrieval in local time
+ # This is used to search for files in a given time range
+
+ # Retrieval in local time + TZO from storage.library
+
+ # Retrieve in UTC from storage.library
+ # This is used to compare files (e.g. syncing) across platforms
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##