diff options
author | Matthias Baumgartner <dev@igsor.net> | 2023-03-05 19:17:00 +0100 |
---|---|---|
committer | Matthias Baumgartner <dev@igsor.net> | 2023-03-05 19:17:00 +0100 |
commit | 5a325565f917c8b1d233d8e6373756c253400909 (patch) | |
tree | e6e0b475c7ab5c6a7ff4f0ea7ad1b08cecf05e68 /test | |
parent | e1e77797454ac747b293f589d8f2e0243173a419 (diff) | |
parent | 98e567933723c59d1d97b3a85e649cfdce514676 (diff) | |
download | tagit-5a325565f917c8b1d233d8e6373756c253400909.tar.gz tagit-5a325565f917c8b1d233d8e6373756c253400909.tar.bz2 tagit-5a325565f917c8b1d233d8e6373756c253400909.zip |
Merge branch 'develop'v0.23.03
Diffstat (limited to 'test')
-rw-r--r-- | test/__init__.py | 0 | ||||
-rw-r--r-- | test/config/__init__.py | 0 | ||||
-rw-r--r-- | test/config/test_schema.py | 284 | ||||
-rw-r--r-- | test/config/test_settings.py | 903 | ||||
-rw-r--r-- | test/config/test_types.py | 251 | ||||
-rw-r--r-- | test/parsing/__init__.py | 0 | ||||
-rw-r--r-- | test/parsing/filter/__init__.py | 0 | ||||
-rw-r--r-- | test/parsing/filter/test_from_string.py | 751 | ||||
-rw-r--r-- | test/parsing/filter/test_to_string.py | 111 | ||||
-rw-r--r-- | test/parsing/test_datefmt.py | 378 | ||||
-rw-r--r-- | test/parsing/test_sort.py | 96 | ||||
-rw-r--r-- | test/utils/__init__.py | 0 | ||||
-rw-r--r-- | test/utils/test_builder.py | 173 | ||||
-rw-r--r-- | test/utils/test_frame.py | 168 | ||||
-rw-r--r-- | test/utils/test_time.py | 159 |
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 ## |