aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bsfs/query/ast/__init__.py4
-rw-r--r--bsfs/query/ast/fetch.py175
-rw-r--r--bsfs/query/ast/filter_.py1
-rw-r--r--test/query/ast_test/test_fetch.py239
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 ##