From 7d62b9cc12239da40ecb77d1b814799cd65873ac Mon Sep 17 00:00:00 2001 From: Mark Dufour Date: Thu, 30 Nov 2023 10:24:12 +0100 Subject: [PATCH] support named function parameters this allows for syntax like: OData.CSC.Intersects(area=geography'SRID=4326;Point(1 2)') -add NamedParam ast Node (with name, param attrs) -add '=' literal -add list_named_parameters grammar (similar to list_expr) -only check args for functions with namespace () or ('geo',) -in django_q visitor, passed NamedParams as keyword args externally, we can now use the following to support above query: class AstToDjangoQVisitorCSC(AstToDjangoQVisitor): def djangofunc_odata__csc__intersects(self, area): return super().djangofunc_geo__intersects(ast.Identifier('footprint'), area) --- odata_query/ast.py | 5 +++ odata_query/django/django_q.py | 10 +++++- odata_query/grammar.py | 64 ++++++++++++++++++++++++---------- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/odata_query/ast.py b/odata_query/ast.py index 90e0716..1189aaf 100644 --- a/odata_query/ast.py +++ b/odata_query/ast.py @@ -320,6 +320,11 @@ class UnaryOp(_Node): ############################################################################### # Function calls ############################################################################### +@dataclass(frozen=True) +class NamedParam(_Node): + name: Identifier + param: _Node + @dataclass(frozen=True) class Call(_Node): func: Identifier diff --git a/odata_query/django/django_q.py b/odata_query/django/django_q.py index af8730e..4a5259e 100644 --- a/odata_query/django/django_q.py +++ b/odata_query/django/django_q.py @@ -265,7 +265,15 @@ def visit_Call(self, node: ast.Call) -> Union[Expression, Q]: except AttributeError: raise ex.UnsupportedFunctionException(func_name) - res = q_gen(*node.args) + args = [] + kwargs = {} + for arg in node.args: + if isinstance(arg, ast.NamedParam): + kwargs[arg.name.name] = arg.param + else: + args.append(arg) + + res = q_gen(*args, **kwargs) return res def visit_CollectionLambda(self, node: ast.CollectionLambda) -> Q: diff --git a/odata_query/grammar.py b/odata_query/grammar.py index 697a190..70e8d6d 100644 --- a/odata_query/grammar.py +++ b/odata_query/grammar.py @@ -96,7 +96,7 @@ class ODataLexer(Lexer): "ALL", "WS", } - literals = {"(", ")", ",", "/", ":"} + literals = {"(", ")", ",", "/", ":", "="} reflags = re.I # Ensure MyPy doesn't lose its mind: @@ -563,23 +563,24 @@ def _function_call(self, func: ast.Identifier, args: List[ast._Node]): func_name = func.full_name() - try: - n_args_exp = ODATA_FUNCTIONS[func_name] - except KeyError: - raise exceptions.UnknownFunctionException(func_name) - - n_args_given = len(args) - if isinstance(n_args_exp, int) and n_args_given != n_args_exp: - raise exceptions.ArgumentCountException( - func_name, n_args_exp, n_args_exp, n_args_given - ) - - if isinstance(n_args_exp, tuple) and ( - n_args_given < n_args_exp[0] or n_args_given > n_args_exp[1] - ): - raise exceptions.ArgumentCountException( - func_name, n_args_exp[0], n_args_exp[1], n_args_given - ) + if func.namespace in ((), ('geo',)): + try: + n_args_exp = ODATA_FUNCTIONS[func_name] + except KeyError: + raise exceptions.UnknownFunctionException(func_name) + + n_args_given = len(args) + if isinstance(n_args_exp, int) and n_args_given != n_args_exp: + raise exceptions.ArgumentCountException( + func_name, n_args_exp, n_args_exp, n_args_given + ) + + if isinstance(n_args_exp, tuple) and ( + n_args_given < n_args_exp[0] or n_args_given > n_args_exp[1] + ): + raise exceptions.ArgumentCountException( + func_name, n_args_exp[0], n_args_exp[1], n_args_given + ) return ast.Call(func, args) @@ -601,6 +602,33 @@ def common_expr(self, p): args = p[1].val return self._function_call(p[0], args) + @_('ODATA_IDENTIFIER "=" common_expr') # type:ignore[no-redef] + def named_param(self, p): + ":meta private:" + return ast.NamedParam(p[0], p.common_expr) + + @_('named_param BWS "," BWS named_param') + def list_named_param(self, p): + ":meta private:" + return [p[0], p[4]] + + @_('list_named_param BWS "," BWS named_param') # type:ignore[no-redef] + def list_named_param(self, p): + ":meta private:" + return p.list_items + [p.named_param] + + @_('ODATA_IDENTIFIER "(" BWS named_param BWS ")"') # type:ignore[no-redef] + def common_expr(self, p): + ":meta private:" + args = [p.named_param] + return self._function_call(p[0], args) + + @_('ODATA_IDENTIFIER "(" BWS list_named_param BWS ")"') # type:ignore[no-redef] + def common_expr(self, p): + ":meta private:" + args = p.list_named_param + return self._function_call(p[0], args) + #################################################################################### # Misc ####################################################################################