diff options
author | Matthias Baumgartner <dev@igsor.net> | 2023-01-20 14:36:11 +0100 |
---|---|---|
committer | Matthias Baumgartner <dev@igsor.net> | 2023-01-20 14:36:11 +0100 |
commit | a4789394e40aaa3152ad6009955709a6c7d277c2 (patch) | |
tree | 404e645ad07c90bff7e6a54f4d8f8e6fee612d3b | |
parent | e12cd52ad267563c8046a593ad551b1dd089a702 (diff) | |
download | bsfs-a4789394e40aaa3152ad6009955709a6c7d277c2.tar.gz bsfs-a4789394e40aaa3152ad6009955709a6c7d277c2.tar.bz2 bsfs-a4789394e40aaa3152ad6009955709a6c7d277c2.zip |
fetch AST
-rw-r--r-- | bsfs/query/ast/__init__.py | 4 | ||||
-rw-r--r-- | bsfs/query/ast/fetch.py | 175 | ||||
-rw-r--r-- | bsfs/query/ast/filter_.py | 1 | ||||
-rw-r--r-- | test/query/ast_test/test_fetch.py | 239 |
4 files changed, 418 insertions, 1 deletions
diff --git a/bsfs/query/ast/__init__.py b/bsfs/query/ast/__init__.py index 704d051..66b097d 100644 --- a/bsfs/query/ast/__init__.py +++ b/bsfs/query/ast/__init__.py @@ -1,6 +1,6 @@ """Query AST components. -The query AST consists of a Filter syntax tree. +The query AST consists of a Filter and a Fetch syntax trees. Classes beginning with an underscore (_) represent internal type hierarchies and should not be used for parsing. Note that the AST structures do not @@ -14,10 +14,12 @@ Author: Matthias Baumgartner, 2022 import typing # inner-module imports +from . import fetch from . import filter_ as filter # pylint: disable=redefined-builtin # exports __all__: typing.Sequence[str] = ( + 'fetch', 'filter', ) diff --git a/bsfs/query/ast/fetch.py b/bsfs/query/ast/fetch.py new file mode 100644 index 0000000..5e603a1 --- /dev/null +++ b/bsfs/query/ast/fetch.py @@ -0,0 +1,175 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +from collections import abc +import typing + +# bsfs imports +from bsfs.utils import URI, typename, normalize_args + +# exports +__all__ : typing.Sequence[str] = ( + 'All', + 'Fetch', + 'FetchExpression', + 'Node', + 'This', + 'Value', + ) + + +## code ## + +class FetchExpression(abc.Hashable): + """Generic Fetch expression.""" + + def __repr__(self) -> str: + """Return the expressions's string representation.""" + return f'{typename(self)}()' + + def __hash__(self) -> int: + """Return the expression's integer representation.""" + return hash(type(self)) + + def __eq__(self, other: typing.Any) -> bool: + """Return True if *self* and *other* are equivalent.""" + return isinstance(other, type(self)) + + +class All(FetchExpression): + """Fetch all child expressions.""" + + # child expressions. + expr: typing.Set[FetchExpression] + + def __init__(self, *expr): + # unpack child expressions + unfolded = set(normalize_args(*expr)) + # check child expressions + if len(unfolded) == 0: + raise AttributeError('expected at least one expression, found none') + if not all(isinstance(itm, FetchExpression) for itm in unfolded): + raise TypeError(expr) + # initialize + super().__init__() + # assign members + self.expr = unfolded + + def __iter__(self) -> typing.Iterator[FetchExpression]: + return iter(self.expr) + + def __len__(self) -> int: + return len(self.expr) + + def __repr__(self) -> str: + return f'{typename(self)}({self.expr})' + + def __hash__(self) -> int: + # FIXME: Produces different hashes for different orders of self.expr + return hash((super().__hash__(), tuple(self.expr))) + + def __eq__(self, other: typing.Any) -> bool: + return super().__eq__(other) and self.expr == other.expr + + +class _Branch(FetchExpression): + """Branch along a predicate.""" + + # FIXME: Use a Predicate (like in ast.filter) so that we can also reverse them! + + # predicate to follow. + predicate: URI + + def __init__(self, predicate: URI): + if not isinstance(predicate, URI): + raise TypeError(predicate) + self.predicate = predicate + + def __repr__(self) -> str: + return f'{typename(self)}({self.predicate})' + + def __hash__(self) -> int: + return hash((super().__hash__(), self.predicate)) + + def __eq__(self, other: typing.Any) -> bool: + return super().__eq__(other) and self.predicate == other.predicate + + +class Fetch(_Branch): + """Follow a predicate before evaluating a child epxression.""" + + # child expression. + expr: FetchExpression + + def __init__(self, predicate: URI, expr: FetchExpression): + # check child expressions + if not isinstance(expr, FetchExpression): + raise TypeError(expr) + # initialize + super().__init__(predicate) + # assign members + self.expr = expr + + def __repr__(self) -> str: + return f'{typename(self)}({self.predicate}, {self.expr})' + + def __hash__(self) -> int: + return hash((super().__hash__(), self.expr)) + + def __eq__(self, other: typing.Any) -> bool: + return super().__eq__(other) and self.expr == other.expr + + +class _Named(_Branch): + """Fetch a (named) symbol at a predicate.""" + + # symbol name. + name: str + + def __init__(self, predicate: URI, name: str): + super().__init__(predicate) + self.name = str(name) + + def __repr__(self) -> str: + return f'{typename(self)}({self.predicate}, {self.name})' + + def __hash__(self) -> int: + return hash((super().__hash__(), self.name)) + + def __eq__(self, other: typing.Any) -> bool: + return super().__eq__(other) and self.name == other.name + + +class Node(_Named): # pylint: disable=too-few-public-methods + """Fetch a Node at a predicate.""" + # FIXME: Is this actually needed? + + +class Value(_Named): # pylint: disable=too-few-public-methods + """Fetch a Literal at a predicate.""" + + +class This(FetchExpression): + """Fetch the current Node.""" + + # symbol name. + name: str + + def __init__(self, name: str): + super().__init__() + self.name = str(name) + + def __repr__(self) -> str: + return f'{typename(self)}({self.name})' + + def __hash__(self) -> int: + return hash((super().__hash__(), self.name)) + + def __eq__(self, other: typing.Any) -> bool: + return super().__eq__(other) and self.name == other.name + +## EOF ## diff --git a/bsfs/query/ast/filter_.py b/bsfs/query/ast/filter_.py index 2f0270c..81b0de2 100644 --- a/bsfs/query/ast/filter_.py +++ b/bsfs/query/ast/filter_.py @@ -153,6 +153,7 @@ class _Agg(FilterExpression, abc.Collection): # check type if not all(isinstance(e, FilterExpression) for e in unfolded): raise TypeError(expr) + # FIXME: Require at least one child expression? # assign member self.expr = unfolded diff --git a/test/query/ast_test/test_fetch.py b/test/query/ast_test/test_fetch.py new file mode 100644 index 0000000..0c48a1f --- /dev/null +++ b/test/query/ast_test/test_fetch.py @@ -0,0 +1,239 @@ +""" + +Part of the tagit test suite. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import unittest + +# bsfs imports +from bsfs.namespace import ns +from bsfs.utils import URI + +# objects to test +from bsfs.query.ast.fetch import FetchExpression +from bsfs.query.ast.fetch import All, This +from bsfs.query.ast.fetch import _Branch, Fetch +from bsfs.query.ast.fetch import _Named, Node, Value + + +## code ## + +class TestExpression(unittest.TestCase): # FetchExpression + def test_essentials(self): + class Foo(FetchExpression): pass + # comparison + self.assertEqual(FetchExpression(), FetchExpression()) + self.assertEqual(hash(FetchExpression()), hash(FetchExpression())) + # comparison respects type + self.assertNotEqual(FetchExpression(), Foo()) + self.assertNotEqual(hash(FetchExpression()), hash(Foo())) + # string conversion + self.assertEqual(str(FetchExpression()), 'FetchExpression()') + self.assertEqual(repr(FetchExpression()), 'FetchExpression()') + self.assertEqual(str(Foo()), 'Foo()') + self.assertEqual(repr(Foo()), 'Foo()') + + +class TestAll(unittest.TestCase): # All + def test_essentials(self): + class Foo(All): pass + expr0 = This('hello') + expr1 = This('world') + # comparison + self.assertEqual(All(expr0), All(expr0)) + self.assertEqual(hash(All(expr0)), hash(All(expr0))) + # comparison respects type + self.assertNotEqual(All(expr0), Foo(expr0)) + self.assertNotEqual(hash(All(expr0)), hash(Foo(expr0))) + # comparison respects expressions + self.assertEqual(All(expr0, expr1), All(expr0, expr1)) + self.assertEqual(hash(All(expr0, expr1)), hash(All(expr0, expr1))) + self.assertNotEqual(All(expr0), All(expr1)) + self.assertNotEqual(hash(All(expr0)), hash(All(expr1))) + # expressions are unordered + self.assertEqual(All(expr0, expr1), All(expr1, expr0)) + self.assertEqual(hash(All(expr0, expr1)), hash(All(expr1, expr0))) + # string conversion + self.assertIn(str(All(expr0, expr1)), { + 'All({This(world), This(hello)})', + 'All({This(hello), This(world)})'}) + self.assertIn(repr(All(expr0, expr1)), { + 'All({This(world), This(hello)})', + 'All({This(hello), This(world)})'}) + + def test_members(self): + class Foo(): pass + expr0 = This('hello') + expr1 = This('world') + # requires at least one child expression + self.assertRaises(AttributeError, All) + # expr returns child expressions + self.assertEqual(All(expr0, expr1).expr, {expr0, expr1}) + # can pass expressions as arguments + self.assertEqual(All(expr0, expr1).expr, {expr0, expr1}) + # can pass a single expression as argument + self.assertEqual(All(expr0).expr, {expr0}) + # can pass expressions as list-like + self.assertEqual(All([expr0, expr1]).expr, {expr0, expr1}) + self.assertEqual(All((expr0, expr1)).expr, {expr0, expr1}) + self.assertEqual(All({expr0, expr1}).expr, {expr0, expr1}) + # can pass a single expression as list-like + self.assertEqual(All([expr0]).expr, {expr0}) + # must pass a FilterExpression + self.assertRaises(TypeError, All, Foo()) + self.assertRaises(TypeError, All, 1234) + self.assertRaises(TypeError, All, 'hello world') + # len returns the number of child expressions + self.assertEqual(len(All(expr0)), 1) + self.assertEqual(len(All(expr0, expr1)), 2) + # iter iterates over child expressions + self.assertSetEqual(set(All(expr0, expr1)), {expr0, expr1}) + + +class TestThis(unittest.TestCase): # This + def test_essentials(self): + class Foo(This): pass + # comparison + self.assertEqual(This('hello'), This('hello')) + self.assertEqual(hash(This('hello')), hash(This('hello'))) + # comparison respects type + self.assertNotEqual(This('hello'), Foo('hello')) + self.assertNotEqual(hash(This('hello')), hash(Foo('hello'))) + # comparison respects name + self.assertNotEqual(This('hello'), This('world')) + self.assertNotEqual(hash(This('hello')), hash(This('world'))) + # string conversion + self.assertEqual(str(This('hello')), 'This(hello)') + self.assertEqual(repr(This('hello')), 'This(hello)') + + def test_members(self): + class Foo(): pass + # name returns member + self.assertEqual(This('hello').name, 'hello') + self.assertEqual(This('world').name, 'world') + # name is converted to a string + self.assertEqual(This(1234).name, '1234') + foo = Foo() + self.assertEqual(This(foo).name, str(foo)) + + +class TestBranch(unittest.TestCase): # _Branch, Fetch + def test_essentials(self): + pred = ns.bse.tag + expr = FetchExpression() + # comparison + self.assertEqual(_Branch(pred), _Branch(pred)) + self.assertEqual(hash(_Branch(pred)), hash(_Branch(pred))) + self.assertEqual(Fetch(pred, expr), Fetch(pred, expr)) + self.assertEqual(hash(Fetch(pred, expr)), hash(Fetch(pred, expr))) + # comparison respects type + self.assertNotEqual(_Branch(pred), Fetch(pred, expr)) + self.assertNotEqual(hash(_Branch(pred)), hash(Fetch(pred, expr))) + self.assertNotEqual(Fetch(pred, expr), _Branch(pred)) + self.assertNotEqual(hash(Fetch(pred, expr)), hash(_Branch(pred))) + # comparison respects predicate + self.assertNotEqual(_Branch(pred), _Branch(ns.bse.filesize)) + self.assertNotEqual(hash(_Branch(pred)), hash(_Branch(ns.bse.filesize))) + self.assertNotEqual(Fetch(pred, expr), Fetch(ns.bse.filesize, expr)) + self.assertNotEqual(hash(Fetch(pred, expr)), hash(Fetch(ns.bse.filesize, expr))) + # comparison respects expression + self.assertNotEqual(Fetch(pred, expr), Fetch(pred, This('foo'))) + self.assertNotEqual(hash(Fetch(pred, expr)), hash(Fetch(pred, This('foo')))) + # string conversion + self.assertEqual(str(_Branch(pred)), f'_Branch({pred})') + self.assertEqual(repr(_Branch(pred)), f'_Branch({pred})') + self.assertEqual(str(Fetch(pred, expr)), f'Fetch({pred}, {expr})') + self.assertEqual(repr(Fetch(pred, expr)), f'Fetch({pred}, {expr})') + + def test_members(self): + class Foo(): pass + pred = ns.bse.tag + expr = FetchExpression() + + # predicate returns member + self.assertEqual(_Branch(pred).predicate, pred) + self.assertEqual(Fetch(pred, expr).predicate, pred) + # can pass an URI + self.assertEqual(_Branch(ns.bse.filename).predicate, ns.bse.filename) + self.assertEqual(Fetch(ns.bse.filename, expr).predicate, ns.bse.filename) + # must pass an URI + self.assertRaises(TypeError, _Branch, Foo()) + self.assertRaises(TypeError, Fetch, Foo(), expr) + # expression returns member + self.assertEqual(Fetch(pred, expr).expr, expr) + # expression must be a FilterExpression + self.assertRaises(TypeError, Fetch, ns.bse.filename, 'hello') + self.assertRaises(TypeError, Fetch, ns.bse.filename, 1234) + self.assertRaises(TypeError, Fetch, ns.bse.filename, Foo()) + + +class TestNamed(unittest.TestCase): # _Named, Node, Value + def test_essentials(self): + pred = ns.bse.tag + name = 'foobar' + # comparison + self.assertEqual(_Named(pred, name), _Named(pred, name)) + self.assertEqual(hash(_Named(pred, name)), hash(_Named(pred, name))) + # comparison respects type + self.assertNotEqual(_Named(pred, name), Node(pred, name)) + self.assertNotEqual(Node(pred, name), Value(pred, name)) + self.assertNotEqual(Value(pred, name), _Named(pred, name)) + self.assertNotEqual(hash(_Named(pred, name)), hash(Node(pred, name))) + self.assertNotEqual(hash(Node(pred, name)), hash(Value(pred, name))) + self.assertNotEqual(hash(Value(pred, name)), hash(_Named(pred, name))) + # comparison respects predicate + self.assertNotEqual(_Named(pred, name), _Named(ns.bse.filesize, name)) + self.assertNotEqual(hash(_Named(pred, name)), hash(_Named(ns.bse.filesize, name))) + self.assertNotEqual(Node(pred, name), Node(ns.bse.filesize, name)) + self.assertNotEqual(hash(Node(pred, name)), hash(Node(ns.bse.filesize, name))) + self.assertNotEqual(Value(pred, name), Value(ns.bse.filesize, name)) + self.assertNotEqual(hash(Value(pred, name)), hash(Value(ns.bse.filesize, name))) + # comparison respects name + self.assertNotEqual(_Named(pred, name), _Named(pred, 'foo')) + self.assertNotEqual(hash(_Named(pred, name)), hash(_Named(pred, 'foo'))) + self.assertNotEqual(Node(pred, name), Node(pred, 'foo')) + self.assertNotEqual(hash(Node(pred, name)), hash(Node(pred, 'foo'))) + self.assertNotEqual(Value(pred, name), Value(pred, 'foo')) + self.assertNotEqual(hash(Value(pred, name)), hash(Value(pred, 'foo'))) + # string conversion + self.assertEqual(str(_Named(pred, name)), f'_Named({pred}, {name})') + self.assertEqual(repr(_Named(pred, name)), f'_Named({pred}, {name})') + self.assertEqual(str(Node(pred, name)), f'Node({pred}, {name})') + self.assertEqual(repr(Node(pred, name)), f'Node({pred}, {name})') + self.assertEqual(str(Value(pred, name)), f'Value({pred}, {name})') + self.assertEqual(repr(Value(pred, name)), f'Value({pred}, {name})') + + def test_members(self): + class Foo(): pass + pred = ns.bse.tag + name = 'foobar' + # predicate returns member + self.assertEqual(_Named(pred, name).predicate, pred) + self.assertEqual(Node(pred, name).predicate, pred) + self.assertEqual(Value(pred, name).predicate, pred) + # can pass an URI as predicate + self.assertEqual(_Named(ns.bse.filename, name).predicate, ns.bse.filename) + self.assertEqual(Node(ns.bse.filename, name).predicate, ns.bse.filename) + self.assertEqual(Value(ns.bse.filename, name).predicate, ns.bse.filename) + # must pass an URI + self.assertRaises(TypeError, _Named, Foo(), name) + self.assertRaises(TypeError, Node, Foo(), name) + self.assertRaises(TypeError, Value, Foo(), name) + # name returns member + self.assertEqual(_Named(pred, name).name, name) + self.assertEqual(Node(pred, name).name, name) + self.assertEqual(Value(pred, name).name, name) + # name is converted to a string + self.assertEqual(_Named(pred, 1234).name, '1234') + self.assertEqual(Node(pred, 1234).name, '1234') + self.assertEqual(Value(pred, 1234).name, '1234') + + +## main ## + +if __name__ == '__main__': + unittest.main() + +## EOF ## |