rebulk 4.2.0__tar.gz → 4.2.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.
Files changed (41) hide show
  1. {rebulk-4.2.0 → rebulk-4.2.2}/PKG-INFO +1 -1
  2. {rebulk-4.2.0 → rebulk-4.2.2}/pyproject.toml +6 -1
  3. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/__version__.py +1 -2
  4. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/chain.py +4 -1
  5. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/loose.py +3 -35
  6. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/match.py +5 -1
  7. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/pattern.py +23 -40
  8. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/rebulk.py +0 -3
  9. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_chain.py +21 -1
  10. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_match.py +13 -1
  11. rebulk-4.2.2/rebulk/test/test_utils.py +29 -0
  12. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/toposort.py +7 -15
  13. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/utils.py +17 -4
  14. {rebulk-4.2.0 → rebulk-4.2.2}/LICENSE +0 -0
  15. {rebulk-4.2.0 → rebulk-4.2.2}/README.md +0 -0
  16. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/__init__.py +0 -0
  17. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/builder.py +0 -0
  18. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/debug.py +0 -0
  19. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/formatters.py +0 -0
  20. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/introspector.py +0 -0
  21. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/key.py +0 -0
  22. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/processors.py +0 -0
  23. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/py.typed +0 -0
  24. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/remodule.py +0 -0
  25. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/rules.py +0 -0
  26. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/__init__.py +0 -0
  27. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/default_rules_module.py +0 -0
  28. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/rebulk_rules_module.py +0 -0
  29. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/rules_module.py +0 -0
  30. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_debug.py +0 -0
  31. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_introspector.py +0 -0
  32. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_key.py +0 -0
  33. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_loose.py +0 -0
  34. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_model.py +0 -0
  35. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_pattern.py +0 -0
  36. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_processors.py +0 -0
  37. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_rebulk.py +0 -0
  38. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_rules.py +0 -0
  39. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_toposort.py +0 -0
  40. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/test/test_validators.py +0 -0
  41. {rebulk-4.2.0 → rebulk-4.2.2}/rebulk/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rebulk
3
- Version: 4.2.0
3
+ Version: 4.2.2
4
4
  Summary: Rebulk - Define simple search patterns in bulk to perform advanced matching on any string.
5
5
  Keywords: re,regexp,regular expression,search,pattern,string,match
6
6
  Author: Rémi Alvergnat
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rebulk"
3
- version = "4.2.0"
3
+ version = "4.2.2"
4
4
  description = "Rebulk - Define simple search patterns in bulk to perform advanced matching on any string."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -105,6 +105,11 @@ select = [
105
105
  python_version = "3.10"
106
106
  files = ["rebulk"]
107
107
  strict = true
108
+
109
+ [[tool.mypy.overrides]]
110
+ # `regex` (the optional `native` extra) ships no type information; scope the
111
+ # relaxation to it instead of silencing missing imports across the whole project.
112
+ module = ["regex.*"]
108
113
  ignore_missing_imports = true
109
114
 
110
115
  [tool.pytest.ini_options]
@@ -3,5 +3,4 @@
3
3
  Version module
4
4
  """
5
5
 
6
- # pragma: no cover
7
- __version__ = "4.2.0"
6
+ __version__ = "4.2.2"
@@ -216,7 +216,10 @@ class Chain(Pattern, Builder):
216
216
  @staticmethod
217
217
  def _group_by_match_index(matches: Iterable[Match]) -> dict[int, list[Match]]:
218
218
  grouped_matches_dict: dict[int, list[Match]] = {}
219
- for match_index, match in itertools.groupby(matches, lambda m: m.match_index):
219
+ # groupby only groups consecutive equal keys, so sort by match_index first
220
+ # to avoid splitting (and overwriting) groups when matches are unordered.
221
+ sorted_matches = sorted(matches, key=lambda m: m.match_index)
222
+ for match_index, match in itertools.groupby(sorted_matches, lambda m: m.match_index):
220
223
  grouped_matches_dict[match_index] = list(match)
221
224
  return grouped_matches_dict
222
225
 
@@ -5,17 +5,9 @@ Various utilities functions
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from inspect import isclass
8
+ from inspect import getfullargspec, isclass
9
9
  from typing import TYPE_CHECKING, Any, cast
10
10
 
11
- try:
12
- from inspect import getfullargspec as getargspec
13
-
14
- _FULLARGSPEC_SUPPORTED = True
15
- except ImportError:
16
- _FULLARGSPEC_SUPPORTED = False
17
- from inspect import getargspec # type: ignore[assignment] # removed in Python 3.11, dead on supported versions
18
-
19
11
  from .utils import is_iterable
20
12
 
21
13
  if TYPE_CHECKING:
@@ -66,7 +58,7 @@ def function_args(callable_: Any, *args: Any, **kwargs: Any) -> tuple[Any, Any]:
66
58
  :return: (args, kwargs) matching the function signature
67
59
  :rtype: tuple
68
60
  """
69
- argspec = getargspec(callable_)
61
+ argspec = getfullargspec(callable_)
70
62
  return argspec_args(argspec, False, *args, **kwargs)
71
63
 
72
64
 
@@ -83,7 +75,7 @@ def constructor_args(class_: Any, *args: Any, **kwargs: Any) -> tuple[Any, Any]:
83
75
  :return: (args, kwargs) matching the function signature
84
76
  :rtype: tuple
85
77
  """
86
- argspec = getargspec(_constructor(class_))
78
+ argspec = getfullargspec(_constructor(class_))
87
79
  return argspec_args(argspec, True, *args, **kwargs)
88
80
 
89
81
 
@@ -107,30 +99,6 @@ def argspec_args(argspec: FullArgSpec, constructor: bool, *args: Any, **kwargs:
107
99
  return call_args, call_kwarg
108
100
 
109
101
 
110
- if not _FULLARGSPEC_SUPPORTED:
111
-
112
- def argspec_args_legacy(argspec: Any, constructor: bool, *args: Any, **kwargs: Any) -> tuple[Any, Any]:
113
- """
114
- Return (args, kwargs) matching the argspec object
115
-
116
- :param argspec: argspec to use
117
- :type argspec: argspec
118
- :param constructor: is it a constructor ?
119
- :type constructor: bool
120
- :param args:
121
- :type args:
122
- :param kwargs:
123
- :type kwargs:
124
- :return: (args, kwargs) matching the function signature
125
- :rtype: tuple
126
- """
127
- call_kwarg = kwargs if argspec.keywords else {k: kwargs[k] for k in kwargs if k in argspec.args}
128
- call_args = args if argspec.varargs else args[: len(argspec.args) - (1 if constructor else 0)]
129
- return call_args, call_kwarg
130
-
131
- argspec_args = argspec_args_legacy
132
-
133
-
134
102
  def ensure_list(param: Any) -> list[Any]:
135
103
  """
136
104
  Retrieves a list from given parameter.
@@ -1164,7 +1164,11 @@ class Match:
1164
1164
  return self.end - self.start
1165
1165
 
1166
1166
  def __hash__(self) -> int:
1167
- return hash(Match) + hash(self.start) + hash(self.end) + hash(self.value)
1167
+ # Hash on the span only (a subset of the __eq__ fields), never on the
1168
+ # mutable value: a match kept in a set/dict must stay findable after its
1169
+ # value is changed. Equal matches still share a hash; same-span matches
1170
+ # with different values collide harmlessly and __eq__ tells them apart.
1171
+ return hash((Match, self.start, self.end))
1168
1172
 
1169
1173
  def __eq__(self, other: object) -> bool:
1170
1174
  if isinstance(other, Match):
@@ -20,6 +20,11 @@ if TYPE_CHECKING:
20
20
  from collections.abc import Callable, Iterator, Sequence
21
21
 
22
22
 
23
+ def _callable_or_none(value: Any) -> Callable[..., Any] | None:
24
+ """Return ``value`` if it is callable, else ``None``."""
25
+ return value if callable(value) else None
26
+
27
+
23
28
  class BasePattern(metaclass=ABCMeta):
24
29
  """
25
30
  Base class for Pattern like objects
@@ -144,21 +149,9 @@ class Pattern(BasePattern, metaclass=ABCMeta):
144
149
  self._log_level = log_level
145
150
  self._properties = properties
146
151
  self.defined_at = debug.defined_at()
147
- self.post_processor: Callable[..., Any] | None
148
- if not callable(post_processor):
149
- self.post_processor = None
150
- else:
151
- self.post_processor = post_processor
152
- self.pre_match_processor: Callable[..., Any] | None
153
- if not callable(pre_match_processor):
154
- self.pre_match_processor = None
155
- else:
156
- self.pre_match_processor = pre_match_processor
157
- self.post_match_processor: Callable[..., Any] | None
158
- if not callable(post_match_processor):
159
- self.post_match_processor = None
160
- else:
161
- self.post_match_processor = post_match_processor
152
+ self.post_processor = _callable_or_none(post_processor)
153
+ self.pre_match_processor = _callable_or_none(pre_match_processor)
154
+ self.post_match_processor = _callable_or_none(post_match_processor)
162
155
 
163
156
  @property
164
157
  def log_level(self) -> int:
@@ -517,32 +510,22 @@ class RePattern(Pattern):
517
510
  for i in range(1, pattern.groups + 1):
518
511
  name = names.get(i, main_match.name)
519
512
  if self.repeated_captures:
520
- for start, end in match_object.spans(i):
521
- child_match = Match(
522
- start,
523
- end,
524
- name=name,
525
- parent=main_match,
526
- pattern=self,
527
- input_string=input_string,
528
- **self._children_match_kwargs,
529
- )
530
- if child_match:
531
- main_match.children.append(child_match)
513
+ spans = match_object.spans(i)
532
514
  else:
533
- start, end = match_object.span(i)
534
- if start > -1 and end > -1:
535
- child_match = Match(
536
- start,
537
- end,
538
- name=name,
539
- parent=main_match,
540
- pattern=self,
541
- input_string=input_string,
542
- **self._children_match_kwargs,
543
- )
544
- if child_match:
545
- main_match.children.append(child_match)
515
+ span = match_object.span(i)
516
+ spans = [span] if span[0] > -1 and span[1] > -1 else []
517
+ for child_start, child_end in spans:
518
+ child_match = Match(
519
+ child_start,
520
+ child_end,
521
+ name=name,
522
+ parent=main_match,
523
+ pattern=self,
524
+ input_string=input_string,
525
+ **self._children_match_kwargs,
526
+ )
527
+ if child_match:
528
+ main_match.children.append(child_match)
546
529
 
547
530
  if main_match:
548
531
  yield main_match
@@ -189,9 +189,6 @@ class Rebulk(Builder):
189
189
  pattern_matches = pattern.matches(cast("str", matches.input_string), context)
190
190
  if pattern_matches:
191
191
  log(pattern.log_level, "Pattern has %s match(es). (%s)", len(pattern_matches), pattern)
192
- else:
193
- pass
194
- # log(pattern.log_level, "Pattern doesn't match. (%s)" % (pattern,))
195
192
  for match in pattern_matches:
196
193
  if match.marker:
197
194
  log(pattern.log_level, "Marker found. (%s)", match)
@@ -8,11 +8,12 @@ from typing import TYPE_CHECKING, Any
8
8
  from rebulk.pattern import FunctionalPattern, RePattern, StringPattern
9
9
 
10
10
  from ..chain import Chain
11
+ from ..match import Match
11
12
  from ..rebulk import Rebulk
12
13
  from ..validators import chars_surround
13
14
 
14
15
  if TYPE_CHECKING:
15
- from ..match import Match, Matches
16
+ from ..match import Matches
16
17
 
17
18
 
18
19
  def test_chain_close() -> None:
@@ -489,3 +490,22 @@ def test_chain_breaker_defaults2() -> None:
489
490
  matches[0].value = 1
490
491
  matches[1].value = 2
491
492
  matches[2].value = 3
493
+
494
+
495
+ def test_group_by_match_index_sorts_unordered_input() -> None:
496
+ # groupby only groups consecutive equal keys, so _group_by_match_index must
497
+ # sort by match_index first; otherwise unordered matches split (and overwrite)
498
+ # groups sharing an index.
499
+ matches: list[Match] = []
500
+ for start, match_index in ((0, 2), (5, 0), (10, 1), (15, 2)):
501
+ match = Match(start, start + 1, name="test")
502
+ match.match_index = match_index
503
+ matches.append(match)
504
+
505
+ grouped = Chain._group_by_match_index(matches)
506
+
507
+ assert sorted(grouped) == [0, 1, 2]
508
+ assert [m.start for m in grouped[0]] == [5]
509
+ assert [m.start for m in grouped[1]] == [10]
510
+ # both match_index=2 matches are kept, in their original relative order
511
+ assert [m.start for m in grouped[2]] == [0, 15]
@@ -51,12 +51,24 @@ class TestMatchClass:
51
51
  other = object()
52
52
 
53
53
  assert hash(match1) != hash(match2)
54
- assert hash(match1) != hash(match3)
54
+ # same span, different value: hashes collide (hash must not depend on the
55
+ # mutable value); equality still tells them apart below.
56
+ assert hash(match1) == hash(match3)
55
57
 
56
58
  assert match1 != other
57
59
  assert match1 != match2
58
60
  assert match1 != match3
59
61
 
62
+ def test_hash_is_stable_under_value_mutation(self) -> None:
63
+ match = Match(1, 3, value="es")
64
+ bucket = {match}
65
+
66
+ match.value = "changed"
67
+
68
+ # The hash must not change when the value is mutated, so the match stays
69
+ # findable in the set it was added to.
70
+ assert match in bucket
71
+
60
72
  def test_length(self) -> None:
61
73
  match1 = Match(0, 4, value="test")
62
74
  match2 = Match(0, 2, value="spanIsUsed")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from ..utils import extend_safe
7
+
8
+
9
+ def test_extend_safe_hashable() -> None:
10
+ target = [1, 2]
11
+ extend_safe(target, [2, 3, 3, 4])
12
+ assert target == [1, 2, 3, 4]
13
+
14
+
15
+ def test_extend_safe_unhashable() -> None:
16
+ # Unhashable elements (e.g. introspected list/dict property values) must
17
+ # still be deduplicated via linear membership instead of raising.
18
+ target: list[Any] = [["a"]]
19
+ extend_safe(target, [["a"], ["b"], ["b"]])
20
+ assert target == [["a"], ["b"]]
21
+
22
+
23
+ def test_extend_safe_mixed_falls_back_without_duplicating() -> None:
24
+ # Hashable elements go through the fast set path; the first unhashable one
25
+ # switches to the linear fallback, and already-added elements are not
26
+ # appended twice.
27
+ target: list[Any] = [1]
28
+ extend_safe(target, [1, 2, ["x"], ["x"], 2])
29
+ assert target == [1, 2, ["x"]]
@@ -1,18 +1,10 @@
1
1
  #!/usr/bin/env python
2
- # Copyright 2014 True Blade Systems, Inc.
2
+ # Vendored from toposort 1.4 (https://bitbucket.org/ericvsmith/toposort),
3
+ # Copyright 2014 True Blade Systems, Inc., licensed under the Apache License,
4
+ # Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0).
3
5
  #
4
- # Licensed under the Apache License, Version 2.0 (the "License");
5
- # you may not use this file except in compliance with the License.
6
- # You may obtain a copy of the License at
7
- #
8
- # http://www.apache.org/licenses/LICENSE-2.0
9
- #
10
- # Original:
11
- # - https://bitbucket.org/ericvsmith/toposort (1.4)
12
- # Modifications:
13
- # - merged Pull request #2 for CyclicDependency error
14
- # - import reduce as original name
15
- # - support python 2.6 dict comprehension
6
+ # Local changes: CyclicDependency error (upstream pull request #2), Python 3
7
+ # only, fully type-annotated.
16
8
 
17
9
  from __future__ import annotations
18
10
 
@@ -27,7 +19,7 @@ _T = TypeVar("_T")
27
19
 
28
20
  class CyclicDependency(ValueError):
29
21
  def __init__(self, cyclic: dict[_T, set[_T]]) -> None:
30
- s = "Cyclic dependencies exist among these items: {}".format(", ".join(repr(x) for x in cyclic.items()))
22
+ s = f"Cyclic dependencies exist among these items: {', '.join(repr(x) for x in cyclic.items())}"
31
23
  super().__init__(s)
32
24
  self.cyclic = cyclic
33
25
 
@@ -38,7 +30,7 @@ def toposort(data: dict[_T, set[_T]]) -> Iterator[set[_T]]:
38
30
  and whose values are a set of dependent items. Output is a list of
39
31
  sets in topological order. The first set consists of items with no
40
32
  dependences, each subsequent set consists of items that depend upon
41
- items in the preceeding sets.
33
+ items in the preceding sets.
42
34
  :param data:
43
35
  :type data:
44
36
  :return:
@@ -102,15 +102,28 @@ def is_iterable(obj: Any) -> bool:
102
102
 
103
103
  def extend_safe(target: list[_T], source: Iterable[_T]) -> None:
104
104
  """
105
- Extends source list to target list only if elements doesn't exists in target list.
105
+ Extends target with elements from source that are not already present.
106
+
107
+ Uses a set for O(1) membership checks when elements are hashable (the common
108
+ case: patterns and rules are deduplicated on every ``matches()`` call), and
109
+ falls back to linear membership when they are not (e.g. unhashable
110
+ introspected property values).
111
+
106
112
  :param target:
107
113
  :type target: list
108
114
  :param source:
109
115
  :type source: list
110
116
  """
111
- for elt in source:
112
- if elt not in target:
113
- target.append(elt)
117
+ try:
118
+ seen = set(target)
119
+ for elt in source:
120
+ if elt not in seen:
121
+ target.append(elt)
122
+ seen.add(elt)
123
+ except TypeError: # unhashable element: fall back to linear membership checks
124
+ for elt in source:
125
+ if elt not in target:
126
+ target.append(elt)
114
127
 
115
128
 
116
129
  class _Ref:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes