rebulk 4.2.2__tar.gz → 4.3.0__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 (42) hide show
  1. {rebulk-4.2.2 → rebulk-4.3.0}/PKG-INFO +16 -2
  2. {rebulk-4.2.2 → rebulk-4.3.0}/README.md +15 -1
  3. {rebulk-4.2.2 → rebulk-4.3.0}/pyproject.toml +1 -1
  4. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/__version__.py +1 -1
  5. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/builder.py +4 -3
  6. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/chain.py +9 -7
  7. rebulk-4.3.0/rebulk/key.py +58 -0
  8. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/match.py +27 -4
  9. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_key.py +18 -0
  10. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_match.py +5 -1
  11. rebulk-4.2.2/rebulk/key.py +0 -45
  12. {rebulk-4.2.2 → rebulk-4.3.0}/LICENSE +0 -0
  13. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/__init__.py +0 -0
  14. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/debug.py +0 -0
  15. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/formatters.py +0 -0
  16. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/introspector.py +0 -0
  17. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/loose.py +0 -0
  18. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/pattern.py +0 -0
  19. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/processors.py +0 -0
  20. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/py.typed +0 -0
  21. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/rebulk.py +0 -0
  22. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/remodule.py +0 -0
  23. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/rules.py +0 -0
  24. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/__init__.py +0 -0
  25. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/default_rules_module.py +0 -0
  26. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/rebulk_rules_module.py +0 -0
  27. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/rules_module.py +0 -0
  28. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_chain.py +0 -0
  29. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_debug.py +0 -0
  30. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_introspector.py +0 -0
  31. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_loose.py +0 -0
  32. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_model.py +0 -0
  33. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_pattern.py +0 -0
  34. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_processors.py +0 -0
  35. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_rebulk.py +0 -0
  36. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_rules.py +0 -0
  37. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_toposort.py +0 -0
  38. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_utils.py +0 -0
  39. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/test/test_validators.py +0 -0
  40. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/toposort.py +0 -0
  41. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/utils.py +0 -0
  42. {rebulk-4.2.2 → rebulk-4.3.0}/rebulk/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rebulk
3
- Version: 4.2.2
3
+ Version: 4.3.0
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
@@ -533,7 +533,10 @@ It has the following additional methods and properties on it.
533
533
  `Match.value` as value.
534
534
 
535
535
  if `enforce_list` is `True`, all values will be wrapped to a list,
536
- even if a single value is found.
536
+ even if a single value is found. This form has a predictable, typed
537
+ shape (`MatchesDict[list]`), unlike the default which is a scalar *or*
538
+ a list per name. For fully typed access, prefer the typed retrieval
539
+ API above (`Key` and `Matches.to(...)`).
537
540
 
538
541
  If `details` is True, `Match.value` objects are replaced with
539
542
  complete `Match` object.
@@ -567,6 +570,17 @@ returns `T | None`, and `matches.all(key)` returns `list[T]`.
567
570
 
568
571
  ```
569
572
 
573
+ For values that aren't built straight from a string, pass an explicit
574
+ `formatter` (a `(str) -> T` converter); the key stays fully typed as `T`.
575
+
576
+ ```python
577
+ >>> from datetime import date
578
+ >>> released = Key("released", date, formatter=date.fromisoformat)
579
+ >>> Rebulk().regex(r'\d{4}-\d{2}-\d{2}', key=released).matches("on 2008-01-02")[released]
580
+ datetime.date(2008, 1, 2)
581
+
582
+ ```
583
+
570
584
  You can also project the matches onto a typed dataclass with `to`. Each field
571
585
  is filled from matches sharing its name: a `list[...]` field collects all
572
586
  values, any other field takes the first, and unmatched fields fall back to
@@ -508,7 +508,10 @@ It has the following additional methods and properties on it.
508
508
  `Match.value` as value.
509
509
 
510
510
  if `enforce_list` is `True`, all values will be wrapped to a list,
511
- even if a single value is found.
511
+ even if a single value is found. This form has a predictable, typed
512
+ shape (`MatchesDict[list]`), unlike the default which is a scalar *or*
513
+ a list per name. For fully typed access, prefer the typed retrieval
514
+ API above (`Key` and `Matches.to(...)`).
512
515
 
513
516
  If `details` is True, `Match.value` objects are replaced with
514
517
  complete `Match` object.
@@ -542,6 +545,17 @@ returns `T | None`, and `matches.all(key)` returns `list[T]`.
542
545
 
543
546
  ```
544
547
 
548
+ For values that aren't built straight from a string, pass an explicit
549
+ `formatter` (a `(str) -> T` converter); the key stays fully typed as `T`.
550
+
551
+ ```python
552
+ >>> from datetime import date
553
+ >>> released = Key("released", date, formatter=date.fromisoformat)
554
+ >>> Rebulk().regex(r'\d{4}-\d{2}-\d{2}', key=released).matches("on 2008-01-02")[released]
555
+ datetime.date(2008, 1, 2)
556
+
557
+ ```
558
+
545
559
  You can also project the matches onto a typed dataclass with `to`. Each field
546
560
  is filled from matches sharing its name: a `list[...]` field collects all
547
561
  values, any other field takes the first, and unmatched fields fall back to
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rebulk"
3
- version = "4.2.2"
3
+ version = "4.3.0"
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"
@@ -3,4 +3,4 @@
3
3
  Version module
4
4
  """
5
5
 
6
- __version__ = "4.2.2"
6
+ __version__ = "4.3.0"
@@ -27,12 +27,13 @@ log = getLogger(__name__).log
27
27
 
28
28
  def _apply_key(key: Key[Any] | None, kwargs: dict[str, Any]) -> dict[str, Any]:
29
29
  """
30
- Inject a typed :class:`~rebulk.key.Key`'s name and value type (used as
31
- formatter) into pattern kwargs, without overriding explicit values.
30
+ Inject a typed :class:`~rebulk.key.Key`'s name and converter (its explicit
31
+ ``formatter`` or its ``value_type``) into pattern kwargs, without overriding
32
+ explicit values.
32
33
  """
33
34
  if key is not None:
34
35
  kwargs.setdefault("name", key.name)
35
- kwargs.setdefault("formatter", key.value_type)
36
+ kwargs.setdefault("formatter", key.converter)
36
37
  return kwargs
37
38
 
38
39
 
@@ -324,7 +324,7 @@ class ChainPart(BasePattern):
324
324
  if max_match_index + 1 < self.repeater_start:
325
325
  raise _InvalidChainException
326
326
 
327
- def chain(self) -> Any:
327
+ def chain(self) -> Chain:
328
328
  """
329
329
  Add patterns chain, using configuration from this chain
330
330
 
@@ -354,7 +354,7 @@ class ChainPart(BasePattern):
354
354
  """
355
355
  return self._hidden
356
356
 
357
- def regex(self, *pattern: Any, **kwargs: Any) -> Any:
357
+ def regex(self, *pattern: Any, **kwargs: Any) -> ChainPart:
358
358
  """
359
359
  Add re pattern
360
360
 
@@ -365,9 +365,11 @@ class ChainPart(BasePattern):
365
365
  :return:
366
366
  :rtype:
367
367
  """
368
- return self._chain.regex(*pattern, **kwargs)
368
+ # The builder's regex/string/functional are typed `-> Self` (Chain), but
369
+ # Chain.pattern() yields a ChainPart, which is what is actually returned.
370
+ return cast("ChainPart", self._chain.regex(*pattern, **kwargs))
369
371
 
370
- def functional(self, *pattern: Any, **kwargs: Any) -> Any:
372
+ def functional(self, *pattern: Any, **kwargs: Any) -> ChainPart:
371
373
  """
372
374
  Add functional pattern
373
375
 
@@ -378,9 +380,9 @@ class ChainPart(BasePattern):
378
380
  :return:
379
381
  :rtype:
380
382
  """
381
- return self._chain.functional(*pattern, **kwargs)
383
+ return cast("ChainPart", self._chain.functional(*pattern, **kwargs))
382
384
 
383
- def string(self, *pattern: Any, **kwargs: Any) -> Any:
385
+ def string(self, *pattern: Any, **kwargs: Any) -> ChainPart:
384
386
  """
385
387
  Add string pattern
386
388
 
@@ -391,7 +393,7 @@ class ChainPart(BasePattern):
391
393
  :return:
392
394
  :rtype:
393
395
  """
394
- return self._chain.string(*pattern, **kwargs)
396
+ return cast("ChainPart", self._chain.string(*pattern, **kwargs))
395
397
 
396
398
  def close(self) -> Any:
397
399
  """
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Typed keys binding a match name to its value type for type-safe retrieval.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, is_dataclass
9
+ from typing import TYPE_CHECKING, Generic, TypeVar, is_typeddict
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Callable
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Key(Generic[T]):
19
+ r"""
20
+ A typed handle for a named match.
21
+
22
+ ``name`` is the match name to declare and look up; ``value_type`` is the type
23
+ of the match value (e.g. ``int``, ``str``, ``float``). ``formatter``, if
24
+ given, is the ``(str) -> T`` converter applied to the matched substring; when
25
+ omitted it defaults to ``value_type`` itself (so ``Key("year", int)`` casts
26
+ with ``int``). Use ``formatter`` for values not constructible straight from a
27
+ string, e.g. ``Key("released", date, formatter=date.fromisoformat)``.
28
+
29
+ Passing a key to a builder method (``key=...``) wires up both the match
30
+ ``name`` and the formatter, so that ``matches[key]`` and ``matches.all(key)``
31
+ return precise types instead of ``Any``.
32
+
33
+ ``value_type`` must be a scalar type (it/the formatter is applied to a single
34
+ matched substring). Structured types (``dataclass`` / ``TypedDict``) cannot
35
+ be built from one string and are rejected here; assemble them from several
36
+ named matches with :meth:`Matches.to` instead.
37
+
38
+ >>> from rebulk import Rebulk, Key
39
+ >>> year = Key("year", int)
40
+ >>> Rebulk().regex(r"\d{4}", key=year).matches("born in 1984")[year]
41
+ 1984
42
+ """
43
+
44
+ name: str
45
+ value_type: type[T]
46
+ formatter: Callable[[str], T] | None = None
47
+
48
+ def __post_init__(self) -> None:
49
+ if is_dataclass(self.value_type) or is_typeddict(self.value_type):
50
+ raise TypeError(
51
+ f"Key value_type must be a scalar type, not {self.value_type!r}; "
52
+ "build structured types from several matches with Matches.to(...) instead"
53
+ )
54
+
55
+ @property
56
+ def converter(self) -> Callable[[str], T]:
57
+ """The ``(str) -> T`` converter: the explicit ``formatter`` or ``value_type``."""
58
+ return self.formatter if self.formatter is not None else self.value_type
@@ -10,7 +10,18 @@ import dataclasses
10
10
  import itertools
11
11
  from collections import OrderedDict, defaultdict
12
12
  from collections.abc import Callable, Iterable, KeysView, MutableSequence
13
- from typing import TYPE_CHECKING, Any, TypeVar, cast, get_args, get_origin, get_type_hints, is_typeddict, overload
13
+ from typing import (
14
+ TYPE_CHECKING,
15
+ Any,
16
+ Literal,
17
+ TypeVar,
18
+ cast,
19
+ get_args,
20
+ get_origin,
21
+ get_type_hints,
22
+ is_typeddict,
23
+ overload,
24
+ )
14
25
 
15
26
  from .debug import defined_at
16
27
  from .key import Key
@@ -22,11 +33,17 @@ if TYPE_CHECKING:
22
33
 
23
34
  T = TypeVar("T")
24
35
  M = TypeVar("M")
36
+ _V = TypeVar("_V")
25
37
 
26
38
 
27
- class MatchesDict(OrderedDict): # type: ignore[type-arg]
39
+ class MatchesDict(OrderedDict[str | None, _V]):
28
40
  """
29
41
  A custom dict with matches property.
42
+
43
+ Generic over the value type: ``to_dict(enforce_list=True)`` returns a
44
+ ``MatchesDict[list[Any]]`` (every name maps to a list), the other modes a
45
+ ``MatchesDict[Any]`` (a name maps to a single value or a list, depending on
46
+ the matches).
30
47
  """
31
48
 
32
49
  def __init__(self) -> None:
@@ -714,7 +731,13 @@ class _BaseMatches(MutableSequence): # type: ignore[type-arg]
714
731
  """
715
732
  return self._tag_dict.keys()
716
733
 
717
- def to_dict(self, details: bool = False, first_value: bool = False, enforce_list: bool = False) -> MatchesDict:
734
+ @overload
735
+ def to_dict(
736
+ self, details: bool = ..., first_value: bool = ..., *, enforce_list: Literal[True]
737
+ ) -> MatchesDict[list[Any]]: ...
738
+ @overload
739
+ def to_dict(self, details: bool = ..., first_value: bool = ..., enforce_list: bool = ...) -> MatchesDict[Any]: ...
740
+ def to_dict(self, details: bool = False, first_value: bool = False, enforce_list: bool = False) -> MatchesDict[Any]:
718
741
  """
719
742
  Converts matches to a dict object.
720
743
  :param details if True, values will be complete Match object, else it will be only string Match.value property
@@ -728,7 +751,7 @@ class _BaseMatches(MutableSequence): # type: ignore[type-arg]
728
751
  :return:
729
752
  :rtype: dict
730
753
  """
731
- ret = MatchesDict()
754
+ ret: MatchesDict[Any] = MatchesDict()
732
755
  for match in sorted(self):
733
756
  value = match if details else match.value
734
757
  ret.matches[match.name].append(match)
@@ -6,6 +6,7 @@ Tests for typed Key retrieval (POC for type-safe value access).
6
6
  from __future__ import annotations
7
7
 
8
8
  from dataclasses import dataclass
9
+ from datetime import date
9
10
  from typing import TYPE_CHECKING, TypedDict
10
11
 
11
12
  import pytest
@@ -31,6 +32,20 @@ def test_key_scalar_retrieval() -> None:
31
32
  assert matches[title] == "Big Buck Bunny"
32
33
 
33
34
 
35
+ def test_key_converter_defaults_to_value_type() -> None:
36
+ assert Key("year", int).converter is int
37
+ fmt = date.fromisoformat
38
+ assert Key("released", date, formatter=fmt).converter is fmt
39
+
40
+
41
+ def test_key_with_explicit_formatter() -> None:
42
+ released = Key("released", date, formatter=date.fromisoformat)
43
+ matches = Rebulk().regex(r"\d{4}-\d{2}-\d{2}", key=released).matches("on 2008-01-02 ...")
44
+
45
+ assert matches[released] == date(2008, 1, 2)
46
+ assert matches.all(released) == [date(2008, 1, 2)]
47
+
48
+
34
49
  def test_key_rejects_structured_value_type() -> None:
35
50
  @dataclass
36
51
  class Movie:
@@ -74,5 +89,8 @@ if TYPE_CHECKING:
74
89
  assert_type(matches[year], "int | None")
75
90
  assert_type(matches.all(year), list[int])
76
91
  assert_type(matches[title], "str | None")
92
+ # A formatter-based key keeps the precise value type.
93
+ released = Key("released", date, formatter=date.fromisoformat)
94
+ assert_type(matches[released], "date | None")
77
95
  # Existing integer / slice access stays intact.
78
96
  assert_type(matches[0], Match)
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  import pytest
8
8
 
@@ -632,3 +632,7 @@ if TYPE_CHECKING:
632
632
  assert_type(matches.named("title", index=0), "Match | None")
633
633
  assert_type(matches.named("title", 0), "Match | None")
634
634
  assert_type(matches.named("title", lambda m: True), "list[Match]")
635
+
636
+ def _to_dict_reveal_types(matches: Matches) -> None:
637
+ # enforce_list=True -> every value is a list (predictable, typable)
638
+ assert_type(matches.to_dict(enforce_list=True)["x"], list[Any])
@@ -1,45 +0,0 @@
1
- #!/usr/bin/env python
2
- """
3
- Typed keys binding a match name to its value type for type-safe retrieval.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from dataclasses import dataclass, is_dataclass
9
- from typing import Generic, TypeVar, is_typeddict
10
-
11
- T = TypeVar("T")
12
-
13
-
14
- @dataclass(frozen=True)
15
- class Key(Generic[T]):
16
- r"""
17
- A typed handle for a named match.
18
-
19
- ``name`` is the match name to declare and look up; ``value_type`` is the type
20
- of the match value and the ``(str) -> T`` converter applied to produce it
21
- (e.g. ``int``, ``str``, ``float``). Passing a key to a builder method
22
- (``key=...``) wires up both the match ``name`` and ``formatter``, so that
23
- ``matches[key]`` and ``matches.all(key)`` return precise types instead of
24
- ``Any``.
25
-
26
- ``value_type`` must be a scalar ``(str) -> T`` converter: it is applied to a
27
- single matched substring. Structured types (``dataclass`` / ``TypedDict``)
28
- cannot be built from one string and are rejected here; assemble them from
29
- several named matches with :meth:`Matches.to` instead.
30
-
31
- >>> from rebulk import Rebulk, Key
32
- >>> year = Key("year", int)
33
- >>> Rebulk().regex(r"\d{4}", key=year).matches("born in 1984")[year]
34
- 1984
35
- """
36
-
37
- name: str
38
- value_type: type[T]
39
-
40
- def __post_init__(self) -> None:
41
- if is_dataclass(self.value_type) or is_typeddict(self.value_type):
42
- raise TypeError(
43
- f"Key value_type must be a scalar (str) -> T converter, not {self.value_type!r}; "
44
- "build structured types from several matches with Matches.to(...) instead"
45
- )
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
File without changes
File without changes
File without changes