omdev 0.0.0.dev439__py3-none-any.whl → 0.0.0.dev486__py3-none-any.whl

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.

Potentially problematic release.


This version of omdev might be problematic. Click here for more details.

Files changed (134) hide show
  1. omdev/.omlish-manifests.json +18 -30
  2. omdev/__about__.py +9 -7
  3. omdev/amalg/gen/gen.py +49 -6
  4. omdev/amalg/gen/imports.py +1 -1
  5. omdev/amalg/gen/manifests.py +1 -1
  6. omdev/amalg/gen/resources.py +1 -1
  7. omdev/amalg/gen/srcfiles.py +13 -3
  8. omdev/amalg/gen/strip.py +1 -1
  9. omdev/amalg/gen/types.py +1 -1
  10. omdev/amalg/gen/typing.py +1 -1
  11. omdev/amalg/info.py +32 -0
  12. omdev/cache/data/actions.py +1 -1
  13. omdev/cache/data/specs.py +1 -1
  14. omdev/cexts/_boilerplate.cc +2 -3
  15. omdev/cexts/cmake.py +4 -1
  16. omdev/ci/cli.py +1 -2
  17. omdev/ci/github/api/v2/api.py +2 -0
  18. omdev/cmdlog/cli.py +1 -2
  19. omdev/dataclasses/_dumping.py +1960 -0
  20. omdev/dataclasses/_template.py +22 -0
  21. omdev/dataclasses/cli.py +6 -1
  22. omdev/dataclasses/codegen.py +340 -60
  23. omdev/dataclasses/dumping.py +200 -0
  24. omdev/interp/uv/provider.py +1 -0
  25. omdev/interp/venvs.py +1 -0
  26. omdev/irc/messages/base.py +50 -0
  27. omdev/irc/messages/formats.py +92 -0
  28. omdev/irc/messages/messages.py +775 -0
  29. omdev/irc/messages/parsing.py +99 -0
  30. omdev/irc/numerics/__init__.py +0 -0
  31. omdev/irc/numerics/formats.py +97 -0
  32. omdev/irc/numerics/numerics.py +865 -0
  33. omdev/irc/numerics/types.py +59 -0
  34. omdev/irc/protocol/LICENSE +11 -0
  35. omdev/irc/protocol/__init__.py +61 -0
  36. omdev/irc/protocol/consts.py +6 -0
  37. omdev/irc/protocol/errors.py +30 -0
  38. omdev/irc/protocol/message.py +21 -0
  39. omdev/irc/protocol/nuh.py +55 -0
  40. omdev/irc/protocol/parsing.py +158 -0
  41. omdev/irc/protocol/rendering.py +153 -0
  42. omdev/irc/protocol/tags.py +102 -0
  43. omdev/irc/protocol/utils.py +30 -0
  44. omdev/manifests/_dumping.py +125 -25
  45. omdev/markdown/__init__.py +0 -0
  46. omdev/markdown/incparse.py +116 -0
  47. omdev/markdown/tokens.py +51 -0
  48. omdev/packaging/marshal.py +8 -8
  49. omdev/packaging/requires.py +6 -6
  50. omdev/packaging/specifiers.py +2 -1
  51. omdev/packaging/versions.py +4 -4
  52. omdev/packaging/wheelfile.py +2 -0
  53. omdev/precheck/blanklines.py +66 -0
  54. omdev/precheck/caches.py +1 -1
  55. omdev/precheck/imports.py +14 -1
  56. omdev/precheck/main.py +4 -3
  57. omdev/precheck/unicode.py +39 -15
  58. omdev/py/asts/__init__.py +0 -0
  59. omdev/py/asts/parents.py +28 -0
  60. omdev/py/asts/toplevel.py +123 -0
  61. omdev/py/asts/visitors.py +18 -0
  62. omdev/py/attrdocs.py +6 -7
  63. omdev/py/bracepy.py +12 -4
  64. omdev/py/reprs.py +32 -0
  65. omdev/py/srcheaders.py +1 -1
  66. omdev/py/tokens/__init__.py +0 -0
  67. omdev/py/tools/mkrelimp.py +1 -1
  68. omdev/py/tools/pipdepup.py +629 -0
  69. omdev/pyproject/pkg.py +190 -45
  70. omdev/pyproject/reqs.py +31 -9
  71. omdev/pyproject/tools/__init__.py +0 -0
  72. omdev/pyproject/tools/aboutdeps.py +55 -0
  73. omdev/pyproject/venvs.py +8 -1
  74. omdev/rs/__init__.py +0 -0
  75. omdev/scripts/ci.py +400 -80
  76. omdev/scripts/interp.py +193 -35
  77. omdev/scripts/lib/__init__.py +0 -0
  78. omdev/scripts/{inject.py → lib/inject.py} +75 -28
  79. omdev/scripts/lib/logs.py +2079 -0
  80. omdev/scripts/{marshal.py → lib/marshal.py} +68 -26
  81. omdev/scripts/pyproject.py +941 -90
  82. omdev/tools/git/cli.py +12 -1
  83. omdev/tools/json/processing.py +5 -2
  84. omdev/tools/jsonview/cli.py +31 -5
  85. omdev/tools/pawk/pawk.py +2 -2
  86. omdev/tools/pip.py +8 -0
  87. omdev/tui/__init__.py +0 -0
  88. omdev/tui/apps/__init__.py +0 -0
  89. omdev/tui/apps/edit/__init__.py +0 -0
  90. omdev/tui/apps/edit/main.py +163 -0
  91. omdev/tui/apps/irc/__init__.py +0 -0
  92. omdev/tui/apps/irc/__main__.py +4 -0
  93. omdev/tui/apps/irc/app.py +278 -0
  94. omdev/tui/apps/irc/client.py +187 -0
  95. omdev/tui/apps/irc/commands.py +175 -0
  96. omdev/tui/apps/irc/main.py +26 -0
  97. omdev/tui/apps/markdown/__init__.py +0 -0
  98. omdev/tui/apps/markdown/__main__.py +11 -0
  99. omdev/{ptk → tui/apps}/markdown/cli.py +5 -7
  100. omdev/tui/rich/__init__.py +34 -0
  101. omdev/tui/rich/console2.py +20 -0
  102. omdev/tui/rich/markdown2.py +186 -0
  103. omdev/tui/textual/__init__.py +226 -0
  104. omdev/tui/textual/app2.py +11 -0
  105. omdev/tui/textual/autocomplete/LICENSE +21 -0
  106. omdev/tui/textual/autocomplete/__init__.py +33 -0
  107. omdev/tui/textual/autocomplete/matching.py +226 -0
  108. omdev/tui/textual/autocomplete/paths.py +202 -0
  109. omdev/tui/textual/autocomplete/widget.py +612 -0
  110. omdev/tui/textual/drivers2.py +55 -0
  111. {omdev-0.0.0.dev439.dist-info → omdev-0.0.0.dev486.dist-info}/METADATA +11 -9
  112. {omdev-0.0.0.dev439.dist-info → omdev-0.0.0.dev486.dist-info}/RECORD +121 -73
  113. omdev/ptk/__init__.py +0 -103
  114. omdev/ptk/apps/ncdu.py +0 -167
  115. omdev/ptk/confirm.py +0 -60
  116. omdev/ptk/markdown/LICENSE +0 -22
  117. omdev/ptk/markdown/__init__.py +0 -10
  118. omdev/ptk/markdown/__main__.py +0 -11
  119. omdev/ptk/markdown/border.py +0 -94
  120. omdev/ptk/markdown/markdown.py +0 -390
  121. omdev/ptk/markdown/parser.py +0 -42
  122. omdev/ptk/markdown/styles.py +0 -29
  123. omdev/ptk/markdown/tags.py +0 -299
  124. omdev/ptk/markdown/utils.py +0 -366
  125. omdev/pyproject/cexts.py +0 -110
  126. /omdev/{ptk/apps → irc}/__init__.py +0 -0
  127. /omdev/{tokens → irc/messages}/__init__.py +0 -0
  128. /omdev/{tokens → py/tokens}/all.py +0 -0
  129. /omdev/{tokens → py/tokens}/tokenizert.py +0 -0
  130. /omdev/{tokens → py/tokens}/utils.py +0 -0
  131. {omdev-0.0.0.dev439.dist-info → omdev-0.0.0.dev486.dist-info}/WHEEL +0 -0
  132. {omdev-0.0.0.dev439.dist-info → omdev-0.0.0.dev486.dist-info}/entry_points.txt +0 -0
  133. {omdev-0.0.0.dev439.dist-info → omdev-0.0.0.dev486.dist-info}/licenses/LICENSE +0 -0
  134. {omdev-0.0.0.dev439.dist-info → omdev-0.0.0.dev486.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,226 @@
1
+ # ruff: noqa: F401
2
+ # flake8: noqa: F401
3
+ from omlish import lang as _lang
4
+
5
+
6
+ with _lang.auto_proxy_init(globals()):
7
+ ##
8
+
9
+ from textual import app # noqa
10
+ from textual import binding # noqa
11
+ from textual import constants # noqa
12
+ from textual import containers # noqa
13
+ from textual import content # noqa
14
+ from textual import driver # noqa
15
+ from textual import events # noqa
16
+ from textual import geometry # noqa
17
+ from textual import markup # noqa
18
+ from textual import message # noqa
19
+ from textual import messages # noqa
20
+ from textual import on # noqa
21
+ from textual import pad # noqa
22
+ from textual import reactive # noqa
23
+ from textual import screen # noqa
24
+ from textual import style # noqa
25
+ from textual import suggester # noqa
26
+ from textual import suggestions # noqa
27
+ from textual import timer # noqa
28
+ from textual import widget # noqa
29
+ from textual import widgets # noqa
30
+ from textual import work # noqa
31
+ from textual.app import ActionError # noqa
32
+ from textual.app import ActiveModeError # noqa
33
+ from textual.app import App as App_ # noqa
34
+ from textual.app import AppError # noqa
35
+ from textual.app import AutopilotCallbackType # noqa
36
+ from textual.app import CallThreadReturnType # noqa
37
+ from textual.app import CommandCallback # noqa
38
+ from textual.app import ComposeResult # noqa
39
+ from textual.app import InvalidModeError # noqa
40
+ from textual.app import InvalidThemeError # noqa
41
+ from textual.app import ModeError # noqa
42
+ from textual.app import RenderResult # noqa
43
+ from textual.app import ReturnType # noqa
44
+ from textual.app import ScreenError # noqa
45
+ from textual.app import ScreenStackError # noqa
46
+ from textual.app import ScreenType # noqa
47
+ from textual.app import SuspendNotSupported # noqa
48
+ from textual.app import SystemCommand # noqa
49
+ from textual.app import UnknownModeError # noqa
50
+ from textual.app import get_system_commands_provider # noqa
51
+ from textual.binding import ActiveBinding # noqa
52
+ from textual.binding import Binding # noqa
53
+ from textual.binding import BindingError # noqa
54
+ from textual.binding import BindingIDString # noqa
55
+ from textual.binding import BindingType # noqa
56
+ from textual.binding import BindingsMap # noqa
57
+ from textual.binding import InvalidBinding # noqa
58
+ from textual.binding import KeyString # noqa
59
+ from textual.binding import Keymap # noqa
60
+ from textual.binding import KeymapApplyResult # noqa
61
+ from textual.binding import NoBinding # noqa
62
+ from textual.containers import Center # noqa
63
+ from textual.containers import CenterMiddle # noqa
64
+ from textual.containers import Container # noqa
65
+ from textual.containers import Grid # noqa
66
+ from textual.containers import Horizontal # noqa
67
+ from textual.containers import HorizontalGroup # noqa
68
+ from textual.containers import HorizontalScroll # noqa
69
+ from textual.containers import ItemGrid # noqa
70
+ from textual.containers import Middle # noqa
71
+ from textual.containers import Right # noqa
72
+ from textual.containers import ScrollableContainer # noqa
73
+ from textual.containers import Vertical # noqa
74
+ from textual.containers import VerticalGroup # noqa
75
+ from textual.containers import VerticalScroll # noqa
76
+ from textual.content import Content # noqa
77
+ from textual.content import ContentText # noqa
78
+ from textual.content import ContentType # noqa
79
+ from textual.content import EMPTY_CONTENT # noqa
80
+ from textual.content import Span # noqa
81
+ from textual.driver import Driver # noqa
82
+ from textual.events import Action # noqa
83
+ from textual.events import AppBlur # noqa
84
+ from textual.events import AppFocus # noqa
85
+ from textual.events import Blur # noqa
86
+ from textual.events import Callback # noqa
87
+ from textual.events import Click # noqa
88
+ from textual.events import Compose # noqa
89
+ from textual.events import CursorPosition # noqa
90
+ from textual.events import DeliveryComplete # noqa
91
+ from textual.events import DeliveryFailed # noqa
92
+ from textual.events import DescendantBlur # noqa
93
+ from textual.events import DescendantFocus # noqa
94
+ from textual.events import Enter # noqa
95
+ from textual.events import Event # noqa
96
+ from textual.events import Focus # noqa
97
+ from textual.events import Hide # noqa
98
+ from textual.events import Idle # noqa
99
+ from textual.events import InputEvent # noqa
100
+ from textual.events import Key # noqa
101
+ from textual.events import Leave # noqa
102
+ from textual.events import Load # noqa
103
+ from textual.events import Mount # noqa
104
+ from textual.events import MouseCapture # noqa
105
+ from textual.events import MouseDown # noqa
106
+ from textual.events import MouseEvent # noqa
107
+ from textual.events import MouseMove # noqa
108
+ from textual.events import MouseRelease # noqa
109
+ from textual.events import MouseScrollDown # noqa
110
+ from textual.events import MouseScrollLeft # noqa
111
+ from textual.events import MouseScrollRight # noqa
112
+ from textual.events import MouseScrollUp # noqa
113
+ from textual.events import MouseUp # noqa
114
+ from textual.events import Paste # noqa
115
+ from textual.events import Print # noqa
116
+ from textual.events import Ready # noqa
117
+ from textual.events import Resize # noqa
118
+ from textual.events import ScreenResume # noqa
119
+ from textual.events import ScreenSuspend # noqa
120
+ from textual.events import Show # noqa
121
+ from textual.events import Timer as TimerEvent # noqa
122
+ from textual.events import Unmount # noqa
123
+ from textual.geometry import NULL_OFFSET # noqa
124
+ from textual.geometry import NULL_REGION # noqa
125
+ from textual.geometry import NULL_SIZE # noqa
126
+ from textual.geometry import NULL_SPACING # noqa
127
+ from textual.geometry import Offset # noqa
128
+ from textual.geometry import Region # noqa
129
+ from textual.geometry import Size # noqa
130
+ from textual.geometry import Spacing # noqa
131
+ from textual.geometry import SpacingDimensions # noqa
132
+ from textual.geometry import clamp # noqa
133
+ from textual.markup import MarkupError # noqa
134
+ from textual.markup import MarkupTokenizer # noqa
135
+ from textual.markup import StyleTokenizer # noqa
136
+ from textual.markup import escape # noqa
137
+ from textual.markup import parse_style # noqa
138
+ from textual.markup import to_content # noqa
139
+ from textual.message import Message # noqa
140
+ from textual.messages import CloseMessages # noqa
141
+ from textual.messages import ExitApp # noqa
142
+ from textual.messages import InBandWindowResize # noqa
143
+ from textual.messages import InvokeLater # noqa
144
+ from textual.messages import Layout # noqa
145
+ from textual.messages import Prompt # noqa
146
+ from textual.messages import Prune # noqa
147
+ from textual.messages import ScrollToRegion # noqa
148
+ from textual.messages import TerminalSupportsSynchronizedOutput # noqa
149
+ from textual.messages import Update # noqa
150
+ from textual.messages import UpdateScroll # noqa
151
+ from textual.pad import HorizontalPad # noqa
152
+ from textual.reactive import Initialize # noqa
153
+ from textual.reactive import Reactive # noqa
154
+ from textual.reactive import ReactiveError # noqa
155
+ from textual.reactive import await_watcher # noqa
156
+ from textual.reactive import invoke_watcher # noqa
157
+ from textual.reactive import reactive as reactive_ # noqa
158
+ from textual.reactive import var # noqa
159
+ from textual.screen import ModalScreen # noqa
160
+ from textual.screen import Screen # noqa
161
+ from textual.screen import SystemModalScreen # noqa
162
+ from textual.style import Style # noqa
163
+ from textual.suggester import SuggestFromList # noqa
164
+ from textual.suggester import Suggester # noqa
165
+ from textual.suggester import SuggestionReady # noqa
166
+ from textual.suggestions import get_suggestion # noqa
167
+ from textual.suggestions import get_suggestions # noqa
168
+ from textual.timer import Timer # noqa
169
+ from textual.timer import TimerCallback # noqa
170
+ from textual.widget import Widget # noqa
171
+ from textual.widgets import Button # noqa
172
+ from textual.widgets import Checkbox # noqa
173
+ from textual.widgets import Collapsible # noqa
174
+ from textual.widgets import ContentSwitcher # noqa
175
+ from textual.widgets import DataTable # noqa
176
+ from textual.widgets import Digits # noqa
177
+ from textual.widgets import DirectoryTree # noqa
178
+ from textual.widgets import Footer # noqa
179
+ from textual.widgets import Header # noqa
180
+ from textual.widgets import HelpPanel # noqa
181
+ from textual.widgets import Input # noqa
182
+ from textual.widgets import KeyPanel # noqa
183
+ from textual.widgets import Label # noqa
184
+ from textual.widgets import Link # noqa
185
+ from textual.widgets import ListItem # noqa
186
+ from textual.widgets import ListView # noqa
187
+ from textual.widgets import LoadingIndicator # noqa
188
+ from textual.widgets import Log # noqa
189
+ from textual.widgets import Markdown # noqa
190
+ from textual.widgets import MarkdownViewer # noqa
191
+ from textual.widgets import MaskedInput # noqa
192
+ from textual.widgets import OptionList # noqa
193
+ from textual.widgets import Placeholder # noqa
194
+ from textual.widgets import Pretty # noqa
195
+ from textual.widgets import ProgressBar # noqa
196
+ from textual.widgets import RadioButton # noqa
197
+ from textual.widgets import RadioSet # noqa
198
+ from textual.widgets import RichLog # noqa
199
+ from textual.widgets import Rule # noqa
200
+ from textual.widgets import Select # noqa
201
+ from textual.widgets import SelectionList # noqa
202
+ from textual.widgets import Sparkline # noqa
203
+ from textual.widgets import Static # noqa
204
+ from textual.widgets import Switch # noqa
205
+ from textual.widgets import Tab # noqa
206
+ from textual.widgets import TabPane # noqa
207
+ from textual.widgets import TabbedContent # noqa
208
+ from textual.widgets import Tabs # noqa
209
+ from textual.widgets import TextArea # noqa
210
+ from textual.widgets import Tooltip # noqa
211
+ from textual.widgets import Tree # noqa
212
+ from textual.widgets import Welcome # noqa
213
+ from textual.widgets.option_list import OptionDoesNotExist # noqa
214
+ from textual.widgets.option_list import Option # noqa
215
+ from textual.widgets.option_list import DuplicateID # noqa
216
+
217
+ ##
218
+
219
+ from .app2 import ( # noqa
220
+ App,
221
+ )
222
+
223
+ from .drivers2 import ( # noqa
224
+ PendingWritesDriverMixin,
225
+ get_pending_writes_driver_class,
226
+ )
@@ -0,0 +1,11 @@
1
+ import typing as ta
2
+
3
+ from textual.app import App as App_
4
+ from textual.binding import BindingType # noqa
5
+
6
+
7
+ ##
8
+
9
+
10
+ class App(App_):
11
+ BINDINGS: ta.ClassVar[ta.Sequence[BindingType]] = App_.BINDINGS # type: ignore[assignment]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Darren Burns
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,33 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2023 Darren Burns
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ # https://github.com/darrenburns/textual-autocomplete/tree/0344cd3eb3383cbbd80e01b035ed808ce53cef4d
19
+ from .matching import ( # noqa
20
+ FuzzySearch,
21
+ FuzzyMatcher,
22
+ )
23
+
24
+ from .paths import ( # noqa
25
+ PathAutoComplete,
26
+ )
27
+
28
+ from .widget import ( # noqa
29
+ AutoCompleteItem,
30
+ AutoCompleteItemHit,
31
+ AutoCompleteList,
32
+ AutoComplete,
33
+ )
@@ -0,0 +1,226 @@
1
+ import functools
2
+ import operator
3
+ import re
4
+ import typing as ta
5
+
6
+ import rich.repr
7
+ from textual.cache import LRUCache
8
+ from textual.content import Content
9
+ from textual.style import Style
10
+
11
+
12
+ ##
13
+
14
+
15
+ class FuzzySearch:
16
+ """
17
+ Performs a fuzzy search.
18
+
19
+ Unlike a regex solution, this will finds all possible matches.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ case_sensitive: bool = False,
25
+ *,
26
+ cache_size: int = 1024 * 4,
27
+ ) -> None:
28
+ """
29
+ Initialize fuzzy search.
30
+
31
+ Args:
32
+ case_sensitive: Is the match case sensitive?
33
+ cache_size: Number of queries to cache.
34
+ """
35
+
36
+ self.case_sensitive = case_sensitive
37
+ self.cache: LRUCache[tuple[str, str], tuple[float, ta.Sequence[int]]] = LRUCache(cache_size)
38
+
39
+ def match(self, query: str, candidate: str) -> tuple[float, ta.Sequence[int]]:
40
+ """
41
+ Match against a query.
42
+
43
+ Args:
44
+ query: The fuzzy query.
45
+ candidate: A candidate to check,.
46
+
47
+ Returns:
48
+ A pair of (score, tuple of offsets). `(0, ())` for no result.
49
+ """
50
+
51
+ cache_key = (query, candidate)
52
+ if cache_key in self.cache:
53
+ return self.cache[cache_key]
54
+ default: tuple[float, ta.Sequence[int]] = (0.0, [])
55
+ result = max(self._match(query, candidate), key=operator.itemgetter(0), default=default)
56
+ self.cache[cache_key] = result
57
+ return result
58
+
59
+ @classmethod
60
+ @functools.lru_cache(maxsize=1024)
61
+ def get_first_letters(cls, candidate: str) -> frozenset[int]:
62
+ return frozenset({match.start() for match in re.finditer(r'\w+', candidate)})
63
+
64
+ def score(self, candidate: str, positions: ta.Sequence[int]) -> float:
65
+ """
66
+ Score a search.
67
+
68
+ Args:
69
+ search: Search object.
70
+
71
+ Returns:
72
+ Score.
73
+ """
74
+
75
+ first_letters = self.get_first_letters(candidate)
76
+ # This is a heuristic, and can be tweaked for better results.
77
+ # Boost first letter matches.
78
+ offset_count = len(positions)
79
+ score: float = offset_count + len(first_letters.intersection(positions))
80
+
81
+ groups = 1
82
+ last_offset, *offsets = positions
83
+ for offset in offsets:
84
+ if offset != last_offset + 1:
85
+ groups += 1
86
+ last_offset = offset
87
+
88
+ # Boost to favor less groups
89
+ normalized_groups = (offset_count - (groups - 1)) / offset_count
90
+ score *= 1 + (normalized_groups * normalized_groups)
91
+ return score
92
+
93
+ def _match(self, query: str, candidate: str) -> ta.Iterable[tuple[float, ta.Sequence[int]]]:
94
+ letter_positions: list[list[int]] = []
95
+ position = 0
96
+
97
+ if not self.case_sensitive:
98
+ candidate = candidate.lower()
99
+ query = query.lower()
100
+ score = self.score
101
+ if query in candidate:
102
+ # Quick exit when the query exists as a substring
103
+ query_location = candidate.rfind(query)
104
+ offsets = list(range(query_location, query_location + len(query)))
105
+ yield (
106
+ score(candidate, offsets) * (2.0 if candidate == query else 1.5),
107
+ offsets,
108
+ )
109
+ return
110
+
111
+ for offset, letter in enumerate(query):
112
+ last_index = len(candidate) - offset
113
+ positions: list[int] = []
114
+ letter_positions.append(positions)
115
+ index = position
116
+ while (location := candidate.find(letter, index)) != -1:
117
+ positions.append(location)
118
+ index = location + 1
119
+ if index >= last_index:
120
+ break
121
+ if not positions:
122
+ yield (0.0, ())
123
+ return
124
+ position = positions[0] + 1
125
+
126
+ possible_offsets: list[list[int]] = []
127
+ query_length = len(query)
128
+
129
+ def get_offsets(offsets: list[int], positions_index: int) -> None:
130
+ """
131
+ Recursively match offsets.
132
+
133
+ Args:
134
+ offsets: A list of offsets.
135
+ positions_index: Index of query letter.
136
+ """
137
+
138
+ for offset in letter_positions[positions_index]:
139
+ if not offsets or offset > offsets[-1]:
140
+ new_offsets = [*offsets, offset]
141
+ if len(new_offsets) == query_length:
142
+ possible_offsets.append(new_offsets)
143
+ else:
144
+ get_offsets(new_offsets, positions_index + 1)
145
+
146
+ get_offsets([], 0)
147
+
148
+ for offsets in possible_offsets:
149
+ yield score(candidate, offsets), offsets
150
+
151
+
152
+ @rich.repr.auto
153
+ class FuzzyMatcher:
154
+ """A fuzzy matcher."""
155
+
156
+ def __init__(
157
+ self,
158
+ query: str,
159
+ *,
160
+ match_style: Style | None = None,
161
+ case_sensitive: bool = False,
162
+ ) -> None:
163
+ """
164
+ Initialize the fuzzy matching object.
165
+
166
+ Args:
167
+ query: A query as typed in by the user.
168
+ match_style: The style to use to highlight matched portions of a string.
169
+ case_sensitive: Should matching be case sensitive?
170
+ """
171
+
172
+ self._query = query
173
+ self._match_style = Style(reverse=True) if match_style is None else match_style
174
+ self._case_sensitive = case_sensitive
175
+ self.fuzzy_search = FuzzySearch()
176
+
177
+ @property
178
+ def query(self) -> str:
179
+ """The query string to look for."""
180
+
181
+ return self._query
182
+
183
+ @property
184
+ def match_style(self) -> Style:
185
+ """The style that will be used to highlight hits in the matched text."""
186
+
187
+ return self._match_style
188
+
189
+ @property
190
+ def case_sensitive(self) -> bool:
191
+ """Is this matcher case sensitive?"""
192
+
193
+ return self._case_sensitive
194
+
195
+ def match(self, candidate: str) -> float:
196
+ """
197
+ Match the candidate against the query.
198
+
199
+ Args:
200
+ candidate: Candidate string to match against the query.
201
+
202
+ Returns:
203
+ Strength of the match from 0 to 1.
204
+ """
205
+
206
+ return self.fuzzy_search.match(self.query, candidate)[0]
207
+
208
+ def highlight(self, candidate: str) -> Content:
209
+ """
210
+ Highlight the candidate with the fuzzy match.
211
+
212
+ Args:
213
+ candidate: The candidate string to match against the query.
214
+
215
+ Returns:
216
+ A [`Text`][rich.text.Text] object with highlighted matches.
217
+ """
218
+
219
+ content = Content.from_markup(candidate)
220
+ score, offsets = self.fuzzy_search.match(self.query, candidate)
221
+ if not score:
222
+ return content
223
+ for offset in offsets:
224
+ if not candidate[offset].isspace():
225
+ content = content.stylize(self._match_style, offset, offset + 1)
226
+ return content