ransacklib 0.1.10__tar.gz → 0.1.11__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: ransacklib
3
- Version: 0.1.10
3
+ Version: 0.1.11
4
4
  Summary: A modern, extensible language for manipulation with structured data
5
5
  Author-email: "Rajmund H. Hruška" <rajmund.hruska@cesnet.cz>
6
6
  License-Expression: MIT
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Operating System :: OS Independent
14
- Requires-Python: >=3.9
14
+ Requires-Python: >=3.10
15
15
  Description-Content-Type: text/x-rst
16
16
  License-File: LICENSE
17
17
  Requires-Dist: lark<1.3.0,>=1.2.2
@@ -19,14 +19,10 @@ Requires-Dist: ipranges<0.2,>=0.1.12
19
19
  Provides-Extra: dev
20
20
  Requires-Dist: pytest; extra == "dev"
21
21
  Requires-Dist: pytest-cov; extra == "dev"
22
- Requires-Dist: flake8; extra == "dev"
23
- Requires-Dist: flake8-pytest-style; extra == "dev"
24
- Requires-Dist: flake8-bugbear; extra == "dev"
25
22
  Requires-Dist: sphinx; extra == "dev"
26
23
  Requires-Dist: sphinx_rtd_theme; extra == "dev"
27
- Requires-Dist: black; extra == "dev"
28
24
  Requires-Dist: mypy; extra == "dev"
29
- Requires-Dist: isort; extra == "dev"
25
+ Requires-Dist: ruff; extra == "dev"
30
26
  Dynamic: license-file
31
27
 
32
28
  Welcome to ransack
@@ -12,7 +12,7 @@ dependencies = [
12
12
  "lark>=1.2.2,<1.3.0",
13
13
  "ipranges>=0.1.12,<0.2",
14
14
  ]
15
- requires-python = ">=3.9"
15
+ requires-python = ">=3.10"
16
16
  authors = [
17
17
  {name = "Rajmund H. Hruška", email = "rajmund.hruska@cesnet.cz"}
18
18
  ]
@@ -27,14 +27,10 @@ classifiers = [
27
27
  dev = [
28
28
  "pytest",
29
29
  "pytest-cov",
30
- "flake8",
31
- "flake8-pytest-style",
32
- "flake8-bugbear",
33
30
  "sphinx",
34
31
  "sphinx_rtd_theme",
35
- "black",
36
32
  "mypy",
37
- "isort",
33
+ "ruff",
38
34
  ]
39
35
 
40
36
  [project.urls]
@@ -54,5 +50,35 @@ ransack = ["py.typed"]
54
50
  [tool.mypy]
55
51
  mypy_path = "$MYPY_CONFIG_FILE_DIR/typings"
56
52
 
57
- [tool.isort]
58
- profile = "black"
53
+ [tool.ruff.lint]
54
+ select = [
55
+ "E", # pycodestyle errors
56
+ "W", # pycodestyle warnings
57
+ "F", # pyflakes
58
+ "I", # isort
59
+ "UP", # pyupgrade
60
+ "PL", # pylint
61
+ "PERF", # Perflint
62
+ "FURB", # refurb
63
+ "RUF", # ruff-specific rules
64
+ "FLY", # flynt
65
+ "B", # flake8-bugbear
66
+ "A", # flake8-builtins,
67
+ "C4", # flake8-comprehensions
68
+ "DTZ", # flake8-datetimez
69
+ "LOG", # flake8-logging
70
+ "G", # flake8-logging-format
71
+ "PIE", # flake8-pie
72
+ "PYI", # flake8-pyi
73
+ "PT", # flake8-pytest-style
74
+ "RET", # flake8-return
75
+ "SIM", # flake8-simplify
76
+ "TC", # flake8-type-checking
77
+ "ARG", # flake8-unused-arguments
78
+ "PTH", # flake8-use-pathlib
79
+ ]
80
+
81
+ ignore = [
82
+ "PLR0913", # too-many-arguments
83
+ "UP038", # deprecated, will be removed
84
+ ]
@@ -2,14 +2,14 @@ from .exceptions import EvaluationError, ParseError, RansackError, ShapeError
2
2
  from .parser import Parser
3
3
  from .transformer import Filter, get_values
4
4
 
5
- __version__ = "0.1.10"
5
+ __version__ = "0.1.11"
6
6
 
7
7
  __all__ = (
8
- "get_values",
9
- "Parser",
8
+ "EvaluationError",
10
9
  "Filter",
11
- "RansackError",
12
10
  "ParseError",
11
+ "Parser",
12
+ "RansackError",
13
13
  "ShapeError",
14
- "EvaluationError",
14
+ "get_values",
15
15
  )
@@ -1,6 +1,6 @@
1
1
  """Custom exceptions for Ransack query parsing, evaluation, and shape validation."""
2
2
 
3
- from typing import Any, Optional
3
+ from typing import Any
4
4
 
5
5
 
6
6
  class RansackError(Exception):
@@ -14,11 +14,11 @@ class PositionInfoMixin:
14
14
  self,
15
15
  line: int,
16
16
  column: int,
17
- context: Optional[str] = None,
18
- start_pos: Optional[int] = None,
19
- end_pos: Optional[int] = None,
20
- end_line: Optional[int] = None,
21
- end_column: Optional[int] = None,
17
+ context: str | None = None,
18
+ start_pos: int | None = None,
19
+ end_pos: int | None = None,
20
+ end_line: int | None = None,
21
+ end_column: int | None = None,
22
22
  *args,
23
23
  **kwargs,
24
24
  ):
@@ -36,7 +36,9 @@ class ParseError(PositionInfoMixin, RansackError):
36
36
  """Raised when the input query cannot be parsed."""
37
37
 
38
38
  def __str__(self):
39
- return f"Syntax error at line {self.line}, column {self.column}.\n\n{self.context}" # noqa
39
+ return (
40
+ f"Syntax error at line {self.line}, column {self.column}.\n\n{self.context}"
41
+ )
40
42
 
41
43
 
42
44
  class ShapeError(PositionInfoMixin, RansackError):
@@ -1,16 +1,15 @@
1
- import re
2
- from collections.abc import MutableSequence
1
+ from collections.abc import Callable, MutableSequence
3
2
  from datetime import datetime, timedelta, timezone
4
3
  from functools import partial
5
4
  from numbers import Number
6
5
  from operator import add, eq, ge, gt, le, lt, mod, mul, sub, truediv
7
- from typing import Any, Callable, Union
6
+ from typing import Any
8
7
 
9
8
  from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range
10
9
 
11
10
  from .exceptions import OperatorNotFoundError
12
11
 
13
- IP = Union[IP4, IP4Net, IP4Range, IP6, IP6Net, IP6Range]
12
+ IP = IP4 | IP4Net | IP4Range | IP6 | IP6Net | IP6Range
14
13
  Operand = type | str
15
14
 
16
15
  commutative_operators = {"+", "*"}
@@ -189,7 +188,7 @@ def _comp_ip_ip(op: str, ip1: IP, ip2: IP) -> bool:
189
188
  case "<=":
190
189
  return ip1.low() <= ip2.high()
191
190
  case "=":
192
- return ip1.low() == ip2.low() and ip1.high() == ip2.high()
191
+ return ip1 in ip2 or ip2 in ip1
193
192
  case _:
194
193
  raise OperatorNotFoundError(op, ("ip", "ip"), (ip1, ip2))
195
194
 
@@ -349,10 +348,12 @@ def _get_comp_dict(op: str, comp: Callable) -> dict[tuple[Operand, Operand], Cal
349
348
  (datetime, tuple): partial(_comp_scalar_range, op),
350
349
  (tuple, Number): partial(_comp_range_scalar, op),
351
350
  (tuple, datetime): partial(_comp_range_scalar, op),
351
+ (str, MutableSequence): partial(_comp_scalar_list, op),
352
352
  ("ip", MutableSequence): partial(_comp_scalar_list, op),
353
353
  (Number, MutableSequence): partial(_comp_scalar_list, op),
354
354
  (datetime, MutableSequence): partial(_comp_scalar_list, op),
355
355
  (timedelta, MutableSequence): partial(_comp_scalar_list, op),
356
+ (MutableSequence, str): partial(_comp_list_scalar, op),
356
357
  (MutableSequence, "ip"): partial(_comp_list_scalar, op),
357
358
  (MutableSequence, Number): partial(_comp_list_scalar, op),
358
359
  (MutableSequence, datetime): partial(_comp_list_scalar, op),
@@ -421,7 +422,7 @@ _operator_map: dict[str, dict[tuple[Operand, Operand], Callable]] = {
421
422
  ">=": _get_comp_dict(">=", ge),
422
423
  "<=": _get_comp_dict("<=", le),
423
424
  "<": _get_comp_dict("<", lt),
424
- "=": _get_comp_dict("=", eq),
425
+ "=": _get_comp_dict("=", eq) | {(str, str): eq},
425
426
  ".": {
426
427
  (str, str): _concat,
427
428
  (MutableSequence, MutableSequence): _concat,
@@ -430,7 +431,6 @@ _operator_map: dict[str, dict[tuple[Operand, Operand], Callable]] = {
430
431
  (str, str): lambda value, pattern: pattern in value,
431
432
  (MutableSequence, str): lambda t, x: any(x in elem for elem in t),
432
433
  },
433
- "like": {(str, str): lambda data, pattern: re.match(pattern, data) is not None},
434
434
  "in": {
435
435
  ("ip", "ip"): lambda left, right: left in right,
436
436
  (str, MutableSequence): _in_scalar_list,
@@ -11,6 +11,8 @@ Classes:
11
11
  input data into an abstract syntax tree (AST).
12
12
  """
13
13
 
14
+ import os
15
+ from pathlib import Path
14
16
  from typing import Any, no_type_check
15
17
 
16
18
  from lark import Lark, Token, Tree
@@ -37,24 +39,23 @@ class Parser:
37
39
  ?start: or_expr
38
40
 
39
41
  ?or_expr: and_expr
40
- | or_expr ("or" | "||") and_expr -> or_op
42
+ | or_expr ("or"i | "||") and_expr -> or_op
41
43
 
42
44
  ?and_expr: not_expr
43
- | and_expr ("and" | "&&") not_expr -> and_op
45
+ | and_expr ("and"i | "&&") not_expr -> and_op
44
46
 
45
47
  ?not_expr: comparison
46
- | ("not" | "!") comparison -> not_op
48
+ | ("not"i | "!") comparison -> not_op
47
49
 
48
50
  ?comparison: sum
49
- | sum ">" sum -> gt
50
- | sum ">=" sum -> gte
51
- | sum "<" sum -> lt
52
- | sum "<=" sum -> lte
53
- | sum "=" sum -> any_eq
54
- | sum "==" sum -> eq
55
- | sum ("like" | "LIKE") sum -> like_op
56
- | sum ("in" | "IN") sum -> in_op
57
- | sum ("contains" | "CONTAINS") sum -> contains_op
51
+ | sum ">" sum -> gt
52
+ | sum ">=" sum -> gte
53
+ | sum "<" sum -> lt
54
+ | sum "<=" sum -> lte
55
+ | sum "=" sum -> any_eq
56
+ | sum "==" sum -> eq
57
+ | sum "in"i sum -> in_op
58
+ | sum "contains"i sum -> contains_op
58
59
 
59
60
  ?sum: product
60
61
  | sum "+" product -> add
@@ -93,11 +94,11 @@ class Parser:
93
94
  | IPV6_RANGE -> ipv6_range
94
95
  | IPV6_CIDR -> ipv6_cidr
95
96
 
96
- datetime: DATE ("T" | "t")? TIME -> datetime_full
97
- | DATE -> datetime_only_date
97
+ datetime: DATE "T"i? TIME -> datetime_full
98
+ | DATE -> datetime_only_date
98
99
 
99
100
  function: FUNCTION [args] ")"
100
- args: atom ("," atom)*
101
+ args: or_expr ("," or_expr)*
101
102
 
102
103
  list: "[" [atom ("," atom)*] "]"
103
104
 
@@ -112,7 +113,7 @@ class Parser:
112
113
  TIME.2: /[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:[Zz]|(?:[+-][0-9]{2}:[0-9]{2}))?/
113
114
  TIMEDELTA.2: /([0-9]+[D|d])?[0-9]{2}:[0-9]{2}:[0-9]{2}/
114
115
  STRING: /"([^"]+)"|\'([^\']+)\'/
115
- VARIABLE: /\.?[_a-zA-Z][-_a-zA-Z0-9]*(?:\.?[a-zA-Z][-_a-zA-Z0-9]*)*/
116
+ VARIABLE: /\.?[_a-zA-Z][-_a-zA-Z0-9]*(?:\.?[_a-zA-Z][-_a-zA-Z0-9]*)*/
116
117
  FUNCTION.2: /[_a-zA-Z][_a-zA-Z0-9]*\(/
117
118
 
118
119
  %import common.WS
@@ -128,7 +129,19 @@ class Parser:
128
129
  be referenced in queries. Context variables override data variables unless
129
130
  the data variable is explicitly accessed with a leading dot.
130
131
  """
131
- self.parser = Lark(self.grammar, parser="lalr", propagate_positions=True)
132
+ # Determine the cache file of the grammar. By default, it's in the home
133
+ # directory. But when the environment variable is set, use the value
134
+ # from that variable. This is useful for GitLab CI/CD.
135
+ cache_path = os.getenv("RANSACK_CACHE_PATH")
136
+ if not cache_path:
137
+ cache_path = str(Path("~/.cache/ransack_grammar_cache").expanduser())
138
+
139
+ self.parser = Lark(
140
+ self.grammar,
141
+ parser="lalr",
142
+ propagate_positions=True,
143
+ cache=cache_path,
144
+ )
132
145
  self.shaper = ExpressionTransformer(context)
133
146
 
134
147
  @no_type_check
@@ -21,14 +21,14 @@ Classes:
21
21
  import re
22
22
  from collections.abc import Mapping, MutableSequence
23
23
  from datetime import datetime, timedelta, timezone
24
- from typing import Any, Optional, Tuple
24
+ from typing import Any, cast
25
25
 
26
26
  from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range
27
27
  from lark import Token, Transformer, Tree, v_args
28
28
  from lark.tree import Meta
29
29
  from lark.visitors import Interpreter
30
30
 
31
- from .exceptions import EvaluationError
31
+ from .exceptions import EvaluationError, RansackError
32
32
  from .function import predefined_functions
33
33
  from .operator import binary_operation
34
34
 
@@ -106,8 +106,8 @@ def _create_meta(token: Token | TokenWrapper) -> Meta:
106
106
 
107
107
 
108
108
  def _get_data_value(
109
- path: str, data: Optional[Mapping | MutableSequence]
110
- ) -> Tuple[Any, bool]:
109
+ path: str, data: Mapping | MutableSequence | None
110
+ ) -> tuple[Any, bool]:
111
111
  """
112
112
  Retrieves a value from the data structure (dictionary-like or list-like)
113
113
  based on a dotted path.
@@ -149,7 +149,7 @@ def _get_data_value(
149
149
  return None, False
150
150
  return _get_data_value(remaining_path, data[key])
151
151
 
152
- elif isinstance(data, MutableSequence):
152
+ if isinstance(data, MutableSequence):
153
153
  # Aggregate results from all list elements
154
154
  aggregated: Any = []
155
155
  for item in data:
@@ -166,7 +166,7 @@ def _get_data_value(
166
166
  return None, False
167
167
 
168
168
 
169
- def get_values(data: Optional[Mapping | MutableSequence], path: str) -> list:
169
+ def get_values(data: Mapping | MutableSequence | None, path: str) -> list:
170
170
  """
171
171
  Public API method to retrieve values from a nested data structure
172
172
  using a dotted path. Returns a flat list of values or an empty list
@@ -435,48 +435,6 @@ class ExpressionTransformer(Transformer):
435
435
  return Tree("var_from_context", [self.context[var]], _create_meta(var))
436
436
  return Tree("var_from_data", [TokenWrapper(var, var)], _create_meta(var))
437
437
 
438
- def range_op(self, start: TokenWrapper, end: TokenWrapper):
439
- """
440
- Transforms a range operation (start to end) into an appropriate range object.
441
-
442
- Parameters:
443
- start: The start token of the range.
444
- end: The end token of the range.
445
-
446
- Returns:
447
- A tree node wrapping a range object. If the tokens represent
448
- IP addresses, the range will be transformed into an IP4Range
449
- or IP6Range object, depending on the IP version; otherwise,
450
- it will return a generic range operation node.
451
- """
452
-
453
- def _create_ip_node(range_class) -> Tree[TokenWrapper]:
454
- """
455
- Helper function to create a tree node for IP ranges.
456
-
457
- Parameters:
458
- range_class: The class (IP4Range or IP6Range) to instantiate.
459
-
460
- Returns:
461
- A tree node wrapping the created IP range object.
462
- """
463
- return Tree(
464
- "ip",
465
- [
466
- TokenWrapper(
467
- _add_tokens(_start.token, _end.token),
468
- range_class(f"{_start.token}-{_end.token}"),
469
- )
470
- ],
471
- )
472
-
473
- _start, _end = start.children[0], end.children[0]
474
- if isinstance(_start.real_value, IP4) and isinstance(_end.real_value, IP4):
475
- return _create_ip_node(IP4Range)
476
- elif isinstance(_start.real_value, IP6) and isinstance(_end.real_value, IP6):
477
- return _create_ip_node(IP6Range)
478
- return Tree("range_op", [start, end])
479
-
480
438
 
481
439
  @v_args(inline=True)
482
440
  class Filter(Interpreter):
@@ -489,7 +447,7 @@ class Filter(Interpreter):
489
447
 
490
448
  data = None
491
449
 
492
- def eval(self, tree: Tree, data: Optional[dict] = None):
450
+ def eval(self, tree: Tree, data: dict | None = None):
493
451
  """
494
452
  Evaluates the given tree using the provided data.
495
453
 
@@ -501,6 +459,7 @@ class Filter(Interpreter):
501
459
  The result of evaluating the tree.
502
460
  """
503
461
  self.data = data if data is not None else {}
462
+ self.function_calls: dict[tuple[str, Tree | None], Any] = {}
504
463
 
505
464
  res = self.visit(tree)
506
465
 
@@ -570,6 +529,8 @@ class Filter(Interpreter):
570
529
  def _binary_operation(self, op: str, l_tree: Tree, r_tree: Tree) -> Any:
571
530
  try:
572
531
  return binary_operation(op, self.visit(l_tree), self.visit(r_tree))
532
+ except RansackError:
533
+ raise
573
534
  except Exception as e:
574
535
  raise EvaluationError(
575
536
  str(e),
@@ -659,19 +620,6 @@ class Filter(Interpreter):
659
620
  """
660
621
  return not self.visit(tree)
661
622
 
662
- def like_op(self, l_tree: Tree, r_tree: Tree) -> bool:
663
- """
664
- Checks if a string matches a regular expression pattern.
665
-
666
- Parameters:
667
- l_tree: Subtree representing the string to check.
668
- r_tree: Subtree representing the regex pattern.
669
-
670
- Returns:
671
- True if the string matches the pattern, otherwise False.
672
- """
673
- return self._binary_operation("like", l_tree, r_tree)
674
-
675
623
  def in_op(self, l_tree: Tree, r_tree: Tree) -> bool:
676
624
  """
677
625
  Checks if a value exists within a data structure.
@@ -762,18 +710,25 @@ class Filter(Interpreter):
762
710
  """
763
711
  return [self.visit(x) for x in data.children if x is not None]
764
712
 
765
- def range_op(self, l_tree: Tree, r_tree: Tree) -> tuple:
713
+ def range_op(self, l_tree: Tree, r_tree: Tree) -> tuple | IP4Range | IP6Range:
766
714
  """
767
- Creates a tuple representing a range.
715
+ Creates a tuple representing a range, or IP4Range/IP6Range.
768
716
 
769
717
  Parameters:
770
718
  l_tree: Subtree representing the start of the range.
771
719
  r_tree: Subtree representing the end of the range.
772
720
 
773
721
  Returns:
774
- A tuple containing the start and end values.
722
+ A tuple containing the start and end values or IP4Range/IP6Range
723
+ object for ip ranges.
775
724
  """
776
- return (self.visit(l_tree), self.visit(r_tree))
725
+ start = self.visit(l_tree)
726
+ end = self.visit(r_tree)
727
+ if isinstance(start, IP4) and isinstance(end, IP4):
728
+ return IP4Range(f"{start}-{end}")
729
+ if isinstance(start, IP6) and isinstance(end, IP6):
730
+ return IP6Range(f"{start}-{end}")
731
+ return (start, end)
777
732
 
778
733
  def exists_op(self, path: Token) -> bool:
779
734
  """
@@ -809,6 +764,9 @@ class Filter(Interpreter):
809
764
  """
810
765
  Calls a predefined function with the given arguments.
811
766
 
767
+ Caches the returned value for a given tuple - function name and
768
+ its arguments - so it can return the same value for subsequent calls.
769
+
812
770
  Parameters:
813
771
  name: The name of the function as a TokenWrapper.
814
772
  args: A Tree object containing function arguments or None.
@@ -819,12 +777,17 @@ class Filter(Interpreter):
819
777
  Raises:
820
778
  ValueError: If the function name is not found in predefined functions.
821
779
  """
822
- function_name = name.real_value
780
+ function_name = cast("str", name.real_value)
781
+ # If called again with the same args, return the cached value.
782
+ if (key := (function_name, args)) in self.function_calls:
783
+ return self.function_calls[key]
823
784
  if function_name in predefined_functions:
824
785
  try:
825
- return predefined_functions[function_name](
786
+ res = predefined_functions[function_name](
826
787
  *(self.visit(x) for x in (args.children if args else [])) # type: ignore
827
788
  )
789
+ self.function_calls[key] = res
790
+ return res
828
791
  except TypeError as e:
829
792
  raise EvaluationError(
830
793
  str(e),
@@ -834,7 +797,7 @@ class Filter(Interpreter):
834
797
  end_line=args.meta.end_line if args else name.end_line,
835
798
  end_column=args.meta.end_column if args else name.end_column,
836
799
  end_pos=args.meta.end_pos if args else name.end_pos,
837
- )
800
+ ) from None
838
801
  raise EvaluationError(
839
802
  f"Function '{name.real_value}' was not found.",
840
803
  line=name.line,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ransacklib
3
- Version: 0.1.10
3
+ Version: 0.1.11
4
4
  Summary: A modern, extensible language for manipulation with structured data
5
5
  Author-email: "Rajmund H. Hruška" <rajmund.hruska@cesnet.cz>
6
6
  License-Expression: MIT
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Operating System :: OS Independent
14
- Requires-Python: >=3.9
14
+ Requires-Python: >=3.10
15
15
  Description-Content-Type: text/x-rst
16
16
  License-File: LICENSE
17
17
  Requires-Dist: lark<1.3.0,>=1.2.2
@@ -19,14 +19,10 @@ Requires-Dist: ipranges<0.2,>=0.1.12
19
19
  Provides-Extra: dev
20
20
  Requires-Dist: pytest; extra == "dev"
21
21
  Requires-Dist: pytest-cov; extra == "dev"
22
- Requires-Dist: flake8; extra == "dev"
23
- Requires-Dist: flake8-pytest-style; extra == "dev"
24
- Requires-Dist: flake8-bugbear; extra == "dev"
25
22
  Requires-Dist: sphinx; extra == "dev"
26
23
  Requires-Dist: sphinx_rtd_theme; extra == "dev"
27
- Requires-Dist: black; extra == "dev"
28
24
  Requires-Dist: mypy; extra == "dev"
29
- Requires-Dist: isort; extra == "dev"
25
+ Requires-Dist: ruff; extra == "dev"
30
26
  Dynamic: license-file
31
27
 
32
28
  Welcome to ransack
@@ -4,11 +4,7 @@ ipranges<0.2,>=0.1.12
4
4
  [dev]
5
5
  pytest
6
6
  pytest-cov
7
- flake8
8
- flake8-pytest-style
9
- flake8-bugbear
10
7
  sphinx
11
8
  sphinx_rtd_theme
12
- black
13
9
  mypy
14
- isort
10
+ ruff
@@ -29,7 +29,7 @@ def test_comp_ip_ip():
29
29
  assert _comp_ip_ip(">=", ip3, ip2) is True
30
30
  assert _comp_ip_ip("<=", ip1, ip3) is True
31
31
  assert _comp_ip_ip("=", ip1, ip1) is True
32
- assert _comp_ip_ip("=", ip3, ip1) is False
32
+ assert _comp_ip_ip("=", ip3, ip1) is True
33
33
 
34
34
  # Test invalid operator (this branch cannot be reached indirectly)
35
35
  with pytest.raises(OperatorNotFoundError) as exc_info:
@@ -143,33 +143,6 @@ def test_invalid_logical_expression(parser):
143
143
  parser.parse_only("a or or b") # Invalid expression
144
144
 
145
145
 
146
- # Test for LIKE operator with valid input
147
- def test_like_operator(parser):
148
- tree = parser.parse_only("Source.IP4 LIKE '192.145.*'")
149
- assert tree is not None
150
- assert "like_op" in tree.pretty()
151
-
152
-
153
- # Test for LIKE operator case insensitivity
154
- def test_like_operator_case_insensitive(parser):
155
- tree = parser.parse_only('Source.IP4 like "10.0.*"')
156
- assert tree is not None
157
- assert "like_op" in tree.pretty()
158
-
159
-
160
- # Test for invalid syntax that should raise an error
161
- def test_invalid_like_syntax(parser):
162
- with pytest.raises(ParseError):
163
- parser.parse_only("Source.IP4 LIKE 192.145.*")
164
-
165
-
166
- # Test a more complex expression with AND/OR and LIKE
167
- def test_nested_like_expressions(parser):
168
- tree = parser.parse_only('Source.IP4 LIKE "192.145.*" and IP6 like "fe80*"')
169
- assert tree is not None
170
- assert "and_op" in tree.pretty()
171
-
172
-
173
146
  # Test for IN operator with a valid list
174
147
  def test_in_operator(parser):
175
148
  tree = parser.parse_only("Source.Port IN [22, 80, 443]")
@@ -4,7 +4,7 @@ import pytest
4
4
  from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range
5
5
  from lark import Token
6
6
 
7
- from ransack.exceptions import EvaluationError, ShapeError
7
+ from ransack.exceptions import RansackError, ShapeError
8
8
  from ransack.parser import Parser
9
9
  from ransack.transformer import ExpressionTransformer, Filter, TokenWrapper, get_values
10
10
 
@@ -39,6 +39,7 @@ sample_data = {
39
39
  "SourceResolvedCountry": ["CZ"],
40
40
  "StorageTime": "2023-12-12",
41
41
  "NestedNullKey": None,
42
+ "_private": 123,
42
43
  },
43
44
  }
44
45
 
@@ -106,7 +107,6 @@ class TestGetValues:
106
107
 
107
108
 
108
109
  class TestExpressionTransformer:
109
-
110
110
  def parse_and_assert(
111
111
  self, parser, expr_transformer, expression, expected_type, expected_value
112
112
  ):
@@ -203,19 +203,15 @@ class TestExpressionTransformer:
203
203
 
204
204
 
205
205
  class TestFilter:
206
-
207
- def parse_filter(self, parser, expr_transformer, filter_, expression):
206
+ def parse_filter(self, parser, filter_, expression):
208
207
  """Helper to parse, evaluate atoms to Python object, then evaluate."""
209
208
  tree = parser.parse(expression)
210
- res = filter_.eval(tree, sample_data)
211
- return res
209
+ return filter_.eval(tree, sample_data)
212
210
 
213
- def parse_filter_and_expect_error(
214
- self, parser, expr_transformer, filter_, expression
215
- ):
211
+ def parse_filter_and_expect_error(self, parser, filter_, expression):
216
212
  """Parse the expression and expect an exception to be raised."""
217
213
  tree = parser.parse(expression)
218
- with pytest.raises(EvaluationError):
214
+ with pytest.raises(RansackError):
219
215
  filter_.eval(tree, sample_data)
220
216
 
221
217
  @pytest.mark.parametrize(
@@ -330,6 +326,11 @@ class TestFilter:
330
326
  ("192.168.1.1 < [192.168.1.0, 192.168.1.1]", False),
331
327
  ("192.168.1.1 == [192.168.1.0, 192.168.1.1]", False),
332
328
  ("192.168.1.1 = [192.168.1.0, 192.168.1.1]", True),
329
+ # IP = range
330
+ ("192.168.0.4 = 192.168.0.1..192.168.0.10", True),
331
+ ("192.168.0.1-192.168.0.10 = 192.168.0.4", True),
332
+ ("192.168.0.1-192.168.0.10 = 192.168.0.11", False),
333
+ ("192.168.0.11 = 192.168.0.1..192.168.0.10", False),
333
334
  # Numeric comparisons with tuples
334
335
  ("5 > 5..7", False),
335
336
  ("5 < 3 .. 6", True),
@@ -378,8 +379,6 @@ class TestFilter:
378
379
  ("[192.168.1.1, 192.168.1.3] > 192.168.1.2", True),
379
380
  ("192.168.1.2 < [192.168.1.1, 192.168.0.1]", False),
380
381
  ("192.168.1.1/32 == 192.168.1.1", True),
381
- # Test like operator
382
- ("Description like '.*login.*'", True),
383
382
  # Test in operator
384
383
  ("192.168.0.1 in 192.168.0.0/16", True),
385
384
  ("not 192.168.0.1-192.168.0.10 in 192.168.0.8-192.168.0.12", True),
@@ -406,6 +405,13 @@ class TestFilter:
406
405
  ("192.168.0.1 in [(192.168.0.0..192.168.0.2)]", True),
407
406
  ("2024::10 in [2024::0-2024::10]", True),
408
407
  ("2024-12-12 in [(2024-12-01..2024-12-31)]", True),
408
+ # Test string equality
409
+ ("'abc' == 'abc'", True),
410
+ ("'abc' = 'abc'", True),
411
+ ("'abc' = '_abc_'", False),
412
+ ("'abc' = ['def', 'abc']", True),
413
+ ("'abc' = ['a', 'b', 'c']", False),
414
+ ("['a', 'b', 'c'] = 'b'", True),
409
415
  # Test contains operator
410
416
  ("'abcdefghi' contains 'def'", True),
411
417
  ("not 'abc' contains 'def'", True),
@@ -424,12 +430,15 @@ class TestFilter:
424
430
  # Test alternate IP range
425
431
  ("192.0.0.1..192.0.0.255", IP4Range("192.0.0.1-192.0.0.255")),
426
432
  ("12D3::0 .. 12D3::1", IP6Range("12D3::-12D3::1")),
433
+ # Test range on variables
434
+ ("(ConnCount - 5) .. ConnCount", (-4, 1)),
427
435
  # Test nested data
428
436
  ("ConnCount", 1),
429
437
  ("_Mentat.SourceResolvedCountry", ["CZ"]),
430
438
  ("Source.IP4", ["1.2.3.235", "1.2.3.236"]),
431
439
  ("Credentials.Username", ["root"]),
432
440
  ("_Mentat.StorageTime", "2023-12-12"),
441
+ ("_Mentat._private", 123),
433
442
  # Test None is returned
434
443
  ("NullKey", None),
435
444
  ("_Mentat.NestedNullKey", None),
@@ -451,10 +460,15 @@ class TestFilter:
451
460
  ("len([1, 2, 3]) == 3", True),
452
461
  ("length([])", 0),
453
462
  ("now() > now() - 1D00:00:00", True),
463
+ ("len(not_in_dict??[])", 0),
464
+ ("len([1].[2].[3])", 3),
465
+ # Test that function call returns the same value
466
+ # when used multipe times in one query
467
+ ("now() == now()", True),
454
468
  ],
455
469
  )
456
- def test_filters(self, parser, expr_transformer, filter_, expression, expected):
457
- result = self.parse_filter(parser, expr_transformer, filter_, expression)
470
+ def test_filters(self, parser, filter_, expression, expected):
471
+ result = self.parse_filter(parser, filter_, expression)
458
472
  assert result == expected
459
473
 
460
474
  @pytest.mark.parametrize(
@@ -470,10 +484,9 @@ class TestFilter:
470
484
  "len(12)", # Test wrong type
471
485
  ],
472
486
  )
473
- def test_invalid_cases(self, parser, expr_transformer, filter_, expression):
487
+ def test_invalid_cases(self, parser, filter_, expression):
474
488
  self.parse_filter_and_expect_error(
475
489
  parser,
476
- expr_transformer,
477
490
  filter_,
478
491
  expression,
479
492
  )
File without changes
File without changes
File without changes