lexcql-parser 1.3.0__tar.gz → 1.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lexcql-parser
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: LexCQL Query Grammar and Parser
5
5
  Keywords: LexCQL,FCS,CQL,Query Parser
6
6
  Author: Erik Körner
@@ -107,7 +107,7 @@ Parsed queries can also be checked against their specification conformance.
107
107
 
108
108
  ```python
109
109
  from lexcql import QueryParser
110
- from lexcql.validation import LexCQLValidatorV0_3
110
+ from lexcql.validation import LexCQLValidatorV0_3, SpecificationValidationError
111
111
 
112
112
  parser = QueryParser(enableSourceLocations=True)
113
113
 
@@ -172,9 +172,9 @@ Run style checks:
172
172
  # setup environment
173
173
  uv sync --extra style
174
174
 
175
+ uv run isort --check --diff .
175
176
  uv run black --check .
176
177
  uv run flake8 . --show-source --statistics
177
- uv run isort --check --diff .
178
178
  ```
179
179
 
180
180
  Run tests:
@@ -67,7 +67,7 @@ Parsed queries can also be checked against their specification conformance.
67
67
 
68
68
  ```python
69
69
  from lexcql import QueryParser
70
- from lexcql.validation import LexCQLValidatorV0_3
70
+ from lexcql.validation import LexCQLValidatorV0_3, SpecificationValidationError
71
71
 
72
72
  parser = QueryParser(enableSourceLocations=True)
73
73
 
@@ -132,9 +132,9 @@ Run style checks:
132
132
  # setup environment
133
133
  uv sync --extra style
134
134
 
135
+ uv run isort --check --diff .
135
136
  uv run black --check .
136
137
  uv run flake8 . --show-source --statistics
137
- uv run isort --check --diff .
138
138
  ```
139
139
 
140
140
  Run tests:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lexcql-parser"
3
- version = "1.3.0"
3
+ version = "1.3.2"
4
4
  description = "LexCQL Query Grammar and Parser"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -56,7 +56,7 @@ def antlr_parse(input: str) -> LexParser.QueryContext:
56
56
  # ---------------------------------------------------------------------------
57
57
 
58
58
 
59
- def parse(input: str, enableSourceLocations: bool = True) -> QueryNode:
59
+ def parse(input: str, *, enableSourceLocations: bool = True) -> QueryNode:
60
60
  """Simple wrapper to generate a `QueryParser` and to parse some
61
61
  input string into a `QueryNode`.
62
62
 
@@ -75,7 +75,7 @@ def parse(input: str, enableSourceLocations: bool = True) -> QueryNode:
75
75
 
76
76
 
77
77
  def can_parse(input: str):
78
- """Simple wrapper to check if the input string can be sucsessfully parsed.
78
+ """Simple wrapper to check if the input string can be successfully parsed.
79
79
 
80
80
  Args:
81
81
  input: raw input query string
@@ -166,7 +166,7 @@ def validate(
166
166
  return False
167
167
 
168
168
  errors = []
169
- if not str(ex) == "unable to parse query":
169
+ if not str(ex).startswith("unable to parse query: "):
170
170
  errors.append(ErrorDetail(str(ex)))
171
171
  errors.extend(parser.errors)
172
172
  return errors
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
  from abc import ABCMeta
3
- from abc import abstractmethod
4
3
  from collections import deque
5
4
  from dataclasses import dataclass
6
5
  from enum import Enum
@@ -107,7 +106,7 @@ class QueryVisitor(Generic[_R], metaclass=ABCMeta):
107
106
  # ----------------------------------------------------
108
107
  # same as antlr4.tree.Tree.ParseTreeVisitor
109
108
 
110
- def visitChildren(self, node: "QueryNode") -> _R:
109
+ def visitChildren(self, node: "QueryNode") -> Optional[_R]:
111
110
  result = self.defaultResult()
112
111
  for i in range(node.child_count):
113
112
  if not self.shouldVisitNextChild(node, result):
@@ -121,12 +120,12 @@ class QueryVisitor(Generic[_R], metaclass=ABCMeta):
121
120
  return result
122
121
 
123
122
  def defaultResult(self) -> Optional[_R]:
124
- return None
123
+ return None # type: ignore
125
124
 
126
- def aggregateResult(self, aggregate: _R, nextResult: _R) -> _R:
125
+ def aggregateResult(self, aggregate: Optional[_R], nextResult: Optional[_R]) -> Optional[_R]:
127
126
  return nextResult
128
127
 
129
- def shouldVisitNextChild(self, node: "QueryNode", currentResult: _R) -> bool:
128
+ def shouldVisitNextChild(self, node: "QueryNode", currentResult: Optional[_R]) -> bool:
130
129
  return True
131
130
 
132
131
 
@@ -236,7 +235,7 @@ class SourceLocation:
236
235
  return f"{self.start}:{self.stop}"
237
236
 
238
237
 
239
- class QueryNode(metaclass=ABCMeta):
238
+ class QueryNode(Generic[_R], metaclass=ABCMeta):
240
239
  """Base class for LexCQL expression tree nodes."""
241
240
 
242
241
  def __init__(
@@ -362,9 +361,8 @@ class QueryNode(metaclass=ABCMeta):
362
361
  strrepr += f"@{self.location.start}:{self.location.stop}"
363
362
  return strrepr
364
363
 
365
- @abstractmethod
366
- def accept(self, visitor: QueryVisitor) -> None:
367
- pass
364
+ def accept(self, visitor: QueryVisitor) -> _R:
365
+ return visitor.visit(self)
368
366
 
369
367
 
370
368
  # ---------------------------------------------------------------------------
@@ -402,9 +400,6 @@ class Modifier(QueryNode):
402
400
  parts.append(f"@{self.location.start}:{self.location.stop}")
403
401
  return "".join(parts)
404
402
 
405
- def accept(self, visitor: QueryVisitor) -> None:
406
- visitor.visit(self)
407
-
408
403
 
409
404
  class Relation(QueryNode):
410
405
  """A LexCQL expression tree relation node."""
@@ -445,9 +440,6 @@ class Relation(QueryNode):
445
440
  parts.append(f"@{self.location.start}:{self.location.stop}")
446
441
  return "".join(parts)
447
442
 
448
- def accept(self, visitor: QueryVisitor) -> None:
449
- visitor.visit(self)
450
-
451
443
 
452
444
  class SearchClause(QueryNode):
453
445
  """A LexCQL expression tree search_clause node."""
@@ -500,9 +492,6 @@ class SearchClause(QueryNode):
500
492
  parts.append(f"@{self.location.start}:{self.location.stop}")
501
493
  return "".join(parts)
502
494
 
503
- def accept(self, visitor: QueryVisitor) -> None:
504
- visitor.visit(self)
505
-
506
495
 
507
496
  class SearchClauseGroup(QueryNode):
508
497
  """A LexCQL expression tree search_clausse_group node."""
@@ -581,9 +570,6 @@ class SearchClauseGroup(QueryNode):
581
570
  parts.append(f"@{self.location.start}:{self.location.stop}")
582
571
  return "".join(parts)
583
572
 
584
- def accept(self, visitor: QueryVisitor) -> None:
585
- visitor.visit(self)
586
-
587
573
 
588
574
  class Subquery(QueryNode):
589
575
  """A LexCQL expression tree search_clausse_group node."""
@@ -633,9 +619,6 @@ class Subquery(QueryNode):
633
619
  parts.append(f"@{self.location.start}:{self.location.stop}")
634
620
  return "".join(parts)
635
621
 
636
- def accept(self, visitor: QueryVisitor) -> None:
637
- visitor.visit(self)
638
-
639
622
 
640
623
  # ---------------------------------------------------------------------------
641
624
 
@@ -681,10 +664,12 @@ class ErrorListener(antlr4.error.ErrorListener.ErrorListener):
681
664
  LOGGER.debug("query: %s", self.query)
682
665
  LOGGER.debug(" %s^- %s", " " * pos, msg)
683
666
 
684
- if isinstance(recognizer, LexParser):
685
- LOGGER.debug("symbol: %s", recognizer.symbolicNames[offendingSymbol.type])
686
- LOGGER.debug("literal: %s", recognizer.literalNames[offendingSymbol.type])
687
- LOGGER.debug("token idx: %s", offendingSymbol.tokenIndex)
667
+ if isinstance(offendingSymbol, Token):
668
+ lit_name, sym_name = self._get_token_name(recognizer, offendingSymbol)
669
+ LOGGER.debug(
670
+ f"symbol: literal={lit_name!r} symbol={sym_name!r} @token-idx={offendingSymbol.tokenIndex}"
671
+ )
672
+ LOGGER.debug(f"{recognizer=}")
688
673
 
689
674
  if pos is None:
690
675
  pos = column
@@ -698,6 +683,27 @@ class ErrorListener(antlr4.error.ErrorListener.ErrorListener):
698
683
  )
699
684
  )
700
685
 
686
+ @staticmethod
687
+ def _get_token_name(recognizer: Recognizer, symbol: Optional[Token]):
688
+ if not symbol:
689
+ return (None, None)
690
+ if not isinstance(recognizer, (LexLexer, LexParser)):
691
+ return (None, None)
692
+ if not hasattr(recognizer, "literalNames"):
693
+ return (None, None)
694
+ if not hasattr(recognizer, "symbolicNames"):
695
+ return (None, None)
696
+
697
+ type = symbol.type
698
+ lit_name = sym_name = None
699
+
700
+ if type < len(recognizer.literalNames):
701
+ lit_name = recognizer.literalNames[type]
702
+ if type < len(recognizer.symbolicNames):
703
+ sym_name = recognizer.symbolicNames[type]
704
+
705
+ return (lit_name, sym_name)
706
+
701
707
  def has_errors(self) -> bool:
702
708
  return bool(self.errors)
703
709
 
@@ -1038,7 +1044,7 @@ class ExpressionTreeBuilder(LexParserVisitor):
1038
1044
  class QueryParser:
1039
1045
  """A LexCQL query parser that produces LexCQL expression trees."""
1040
1046
 
1041
- def __init__(self, enableSourceLocations: bool = False):
1047
+ def __init__(self, *, enableSourceLocations: bool = False):
1042
1048
  """[Constructor]
1043
1049
 
1044
1050
  Args:
@@ -1093,7 +1099,8 @@ class QueryParser:
1093
1099
  for msg in error_listener.errors:
1094
1100
  LOGGER.debug("ERROR: %s", msg)
1095
1101
 
1096
- raise QueryParserException("unable to parse query")
1102
+ first_message = error_listener.errors[0].message if error_listener.errors else "?"
1103
+ raise QueryParserException(f"unable to parse query: {first_message}")
1097
1104
  except ExpressionTreeBuilderException as ex:
1098
1105
  raise QueryParserException(str(ex)) from ex
1099
1106
  except QueryParserException:
@@ -97,7 +97,7 @@ class Validator(QueryVisitorAdapter[_R], metaclass=ABCMeta):
97
97
 
98
98
  Returns:
99
99
  bool: ``True`` if query in valid, ``False`` if any error was recorded
100
- in the ``.errors`` attribute
100
+ in the ``.errors`` attribute (or in ``.warnings`` if ``.warnings_as_errors``)
101
101
  """
102
102
  # allows to override the query string here
103
103
  if query is not None:
@@ -105,10 +105,15 @@ class Validator(QueryVisitorAdapter[_R], metaclass=ABCMeta):
105
105
 
106
106
  # reset list of errors
107
107
  self.errors = []
108
+ self.warnings = []
108
109
 
109
110
  self.visit(node)
110
111
 
111
- return len(self.errors) == 0
112
+ num_issues = len(self.errors)
113
+ if self.warnings_as_errors:
114
+ num_issues += len(self.warnings)
115
+
116
+ return num_issues == 0
112
117
 
113
118
  def is_valid(self, node: QueryNode, *, query: Optional[str] = None) -> bool:
114
119
  """Convenience method that simply calls ``.validate()`` and returns
@@ -205,31 +210,31 @@ class Validator(QueryVisitorAdapter[_R], metaclass=ABCMeta):
205
210
  return self.stack[-1]
206
211
  return None
207
212
 
208
- def visit_Subquery(self, node: Subquery) -> _R:
213
+ def visit_Subquery(self, node: Subquery) -> Optional[_R]:
209
214
  self.stack.append(node)
210
215
  result = super().visit_Subquery(node)
211
216
  self.stack.pop()
212
217
  return result
213
218
 
214
- def visit_SearchClauseGroup(self, node: SearchClauseGroup) -> _R:
219
+ def visit_SearchClauseGroup(self, node: SearchClauseGroup) -> Optional[_R]:
215
220
  self.stack.append(node)
216
221
  result = super().visit_SearchClauseGroup(node)
217
222
  self.stack.pop()
218
223
  return result
219
224
 
220
- def visit_SearchClause(self, node: SearchClause) -> _R:
225
+ def visit_SearchClause(self, node: SearchClause) -> Optional[_R]:
221
226
  self.stack.append(node)
222
227
  result = super().visit_SearchClause(node)
223
228
  self.stack.pop()
224
229
  return result
225
230
 
226
- def visit_Relation(self, node: Relation) -> _R:
231
+ def visit_Relation(self, node: Relation) -> Optional[_R]:
227
232
  self.stack.append(node)
228
233
  result = super().visit_Relation(node)
229
234
  self.stack.pop()
230
235
  return result
231
236
 
232
- def visit_Modifier(self, node: Modifier) -> _R:
237
+ def visit_Modifier(self, node: Modifier) -> Optional[_R]:
233
238
  self.stack.append(node)
234
239
  result = super().visit_Modifier(node)
235
240
  self.stack.pop()