PyEventEngine 0.6.0.post3__cp313-cp313-win_amd64.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.
- event_engine/__infra__.pxd +2 -0
- event_engine/__init__.pxd +2 -0
- event_engine/__init__.py +37 -0
- event_engine/base/__infra__.pxd +9 -0
- event_engine/base/__init__.pxd +9 -0
- event_engine/base/__init__.py +82 -0
- event_engine/base/c_allocator_protocol.c +10817 -0
- event_engine/base/c_allocator_protocol.cp313-win_amd64.pyd +0 -0
- event_engine/base/c_allocator_protocol.h +23 -0
- event_engine/base/c_allocator_protocol.pxd +31 -0
- event_engine/base/c_allocator_protocol.pyx +99 -0
- event_engine/capi/__infra__.pxd +189 -0
- event_engine/capi/__init__.pxd +189 -0
- event_engine/capi/__init__.py +84 -0
- event_engine/capi/c_engine.c +28711 -0
- event_engine/capi/c_engine.cp313-win_amd64.pyd +0 -0
- event_engine/capi/c_engine.h +393 -0
- event_engine/capi/c_engine.pxd +84 -0
- event_engine/capi/c_engine.pyi +424 -0
- event_engine/capi/c_engine.pyx +560 -0
- event_engine/capi/c_event.c +19921 -0
- event_engine/capi/c_event.cp313-win_amd64.pyd +0 -0
- event_engine/capi/c_event.h +342 -0
- event_engine/capi/c_event.pxd +159 -0
- event_engine/capi/c_event.pyi +226 -0
- event_engine/capi/c_event.pyx +387 -0
- event_engine/capi/c_topic.c +30978 -0
- event_engine/capi/c_topic.cp313-win_amd64.pyd +0 -0
- event_engine/capi/c_topic.h +1007 -0
- event_engine/capi/c_topic.pxd +147 -0
- event_engine/capi/c_topic.pyi +580 -0
- event_engine/capi/c_topic.pyx +725 -0
- event_engine/capi/fallback_engine.py +670 -0
- event_engine/includes/event_engine/base/c_allocator_protocol.c +10817 -0
- event_engine/includes/event_engine/base/c_allocator_protocol.h +23 -0
- event_engine/includes/event_engine/capi/c_engine.c +28711 -0
- event_engine/includes/event_engine/capi/c_engine.h +393 -0
- event_engine/includes/event_engine/capi/c_event.c +19921 -0
- event_engine/includes/event_engine/capi/c_event.h +342 -0
- event_engine/includes/event_engine/capi/c_topic.c +30978 -0
- event_engine/includes/event_engine/capi/c_topic.h +1007 -0
- event_engine/native/__init__.py +60 -0
- event_engine/native/_event.py +420 -0
- event_engine/native/_topic.py +171 -0
- event_engine/native/engine.py +746 -0
- event_engine/native/event.py +467 -0
- event_engine/native/topic.py +945 -0
- pyeventengine-0.6.0.post3.dist-info/METADATA +189 -0
- pyeventengine-0.6.0.post3.dist-info/RECORD +52 -0
- pyeventengine-0.6.0.post3.dist-info/WHEEL +5 -0
- pyeventengine-0.6.0.post3.dist-info/licenses/LICENSE +21 -0
- pyeventengine-0.6.0.post3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Native Python fallback implementation for PyTopic and related classes.
|
|
3
|
+
|
|
4
|
+
This module provides a pure Python implementation that mimics the behavior of the
|
|
5
|
+
Cython-based c_topic module. It is used as a fallback when the Cython extension
|
|
6
|
+
cannot be compiled (e.g., due to lack of Cython, GCC, or Clang).
|
|
7
|
+
|
|
8
|
+
The API is designed to match event_engine.capi.c_topic as closely as possible.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import enum
|
|
14
|
+
import re
|
|
15
|
+
from collections import deque
|
|
16
|
+
from collections.abc import Iterable, Iterator
|
|
17
|
+
from functools import cached_property
|
|
18
|
+
from typing import Any, TypedDict, overload
|
|
19
|
+
|
|
20
|
+
# Global internal map for topic internalization
|
|
21
|
+
_GLOBAL_INTERNAL_MAP: dict[str, PyTopic] = {}
|
|
22
|
+
_GLOBAL_ALLOCATOR = None
|
|
23
|
+
|
|
24
|
+
# Topic parsing configuration (matching C defaults)
|
|
25
|
+
DEFAULT_TOPIC_SEP = '.'
|
|
26
|
+
DEFAULT_OPTION_SEP = '|'
|
|
27
|
+
DEFAULT_RANGE_BRACKETS = "()"
|
|
28
|
+
DEFAULT_WILDCARD_BRACKETS = "{}"
|
|
29
|
+
DEFAULT_WILDCARD_MARKER = '+'
|
|
30
|
+
DEFAULT_PATTERN_DELIM = '/'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def init_internal_map(default_capacity: int = 1024) -> dict[str, PyTopic]:
|
|
34
|
+
"""Initialize or return a shared internal byte map.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
default_capacity: Default capacity (in bytes) used when creating the internal map.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A ByteMap instance wrapping the shared internal bytemap.
|
|
41
|
+
"""
|
|
42
|
+
global _GLOBAL_INTERNAL_MAP
|
|
43
|
+
# In pure Python, we just use a dict, so capacity is ignored
|
|
44
|
+
return _GLOBAL_INTERNAL_MAP
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def clear_internal_map() -> None:
|
|
48
|
+
"""Clear and free the shared internal bytemap.
|
|
49
|
+
|
|
50
|
+
This releases the internal resources and resets any cached references.
|
|
51
|
+
"""
|
|
52
|
+
global _GLOBAL_INTERNAL_MAP
|
|
53
|
+
_GLOBAL_INTERNAL_MAP.clear()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_internal_topic(key: str, owner: bool = False) -> PyTopic | None:
|
|
57
|
+
"""Get a registered topic from the internal map, if there is any.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
key: the literal of the topic to look up.
|
|
61
|
+
owner: whether to return a copy (True) or the original reference (False).
|
|
62
|
+
In pure Python, ownership is always with the object itself.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
PyTopic instance if found or None.
|
|
66
|
+
"""
|
|
67
|
+
global _GLOBAL_INTERNAL_MAP
|
|
68
|
+
topic = _GLOBAL_INTERNAL_MAP.get(key)
|
|
69
|
+
if topic is None:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
# Return a copy if owner is True, otherwise return reference
|
|
73
|
+
if owner:
|
|
74
|
+
new_topic = PyTopic.__new__(PyTopic)
|
|
75
|
+
new_topic._parts = list(topic._parts)
|
|
76
|
+
new_topic._value = topic._value
|
|
77
|
+
new_topic._hash = topic._hash
|
|
78
|
+
new_topic._is_exact = topic._is_exact
|
|
79
|
+
return new_topic
|
|
80
|
+
|
|
81
|
+
return topic
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_internal_map() -> dict[str, PyTopic]:
|
|
85
|
+
"""Return a dictionary view of the internal topic map.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
A dictionary mapping topic literal strings to PyTopic instances.
|
|
89
|
+
"""
|
|
90
|
+
global _GLOBAL_INTERNAL_MAP
|
|
91
|
+
return _GLOBAL_INTERNAL_MAP.copy()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def init_allocator(init_capacity: int = 4096, with_shm: bool = False) -> None:
|
|
95
|
+
"""Initialize or return the global Allocator.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
init_capacity: Initial capacity (in bytes) for the allocator.
|
|
99
|
+
(Ignored in pure Python implementation)
|
|
100
|
+
with_shm: If True, create an allocator backed by shared memory.
|
|
101
|
+
(Ignored in pure Python implementation)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
None (allocator not used in pure Python implementation)
|
|
105
|
+
|
|
106
|
+
Note:
|
|
107
|
+
This function is provided for API compatibility with the Cython version,
|
|
108
|
+
but does nothing in the pure Python implementation.
|
|
109
|
+
"""
|
|
110
|
+
# No-op in pure Python - allocators are not needed
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class PyTopicType(enum.IntEnum):
|
|
115
|
+
"""Enumeration of topic part types.
|
|
116
|
+
|
|
117
|
+
Maps to the underlying C-level TopicType constants.
|
|
118
|
+
"""
|
|
119
|
+
TOPIC_PART_EXACT = 0
|
|
120
|
+
TOPIC_PART_ANY = 1
|
|
121
|
+
TOPIC_PART_RANGE = 2
|
|
122
|
+
TOPIC_PART_PATTERN = 3
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class PyTopicPart:
|
|
126
|
+
"""Base Python wrapper for a single topic part.
|
|
127
|
+
|
|
128
|
+
In the native Python implementation, all topic parts own their underlying memory.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
__slots__ = ('_next', '_ttype')
|
|
132
|
+
|
|
133
|
+
def __init__(self, *args: Any, alloc: bool = False, **kwargs: Any) -> None:
|
|
134
|
+
"""Create or attach to a topic part.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
alloc: If True, allocate and initialize internal structures.
|
|
138
|
+
"""
|
|
139
|
+
self._next: PyTopicPart | None = None
|
|
140
|
+
self._ttype: PyTopicType = PyTopicType.TOPIC_PART_EXACT
|
|
141
|
+
|
|
142
|
+
def next(self) -> PyTopicPart:
|
|
143
|
+
"""Return the next topic part.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The next PyTopicPart instance.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
StopIteration: If this is the last part.
|
|
150
|
+
"""
|
|
151
|
+
if self._next is None:
|
|
152
|
+
raise StopIteration
|
|
153
|
+
return self._next
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def owner(self) -> bool:
|
|
157
|
+
"""bool: Whether this Python object owns the underlying memory (always True in native Python)."""
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def ttype(self) -> PyTopicType:
|
|
162
|
+
"""int: The topic part type as a PyTopicType."""
|
|
163
|
+
return self._ttype
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def addr(self) -> int:
|
|
167
|
+
"""int: Numeric address / id of the underlying C structure."""
|
|
168
|
+
return id(self)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class PyTopicPartExact(PyTopicPart):
|
|
172
|
+
"""Topic part representing an exact literal segment."""
|
|
173
|
+
|
|
174
|
+
__slots__ = ('_part',)
|
|
175
|
+
|
|
176
|
+
def __init__(self, part: str = None, *args: Any, alloc: bool = False, **kwargs: Any) -> None:
|
|
177
|
+
"""Create an exact topic part.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
part: Literal string to store. If omitted and alloc is True, an empty initialized part is created.
|
|
181
|
+
alloc: If True, allocate underlying structures.
|
|
182
|
+
"""
|
|
183
|
+
super().__init__(*args, alloc=alloc, **kwargs)
|
|
184
|
+
self._ttype = PyTopicType.TOPIC_PART_EXACT
|
|
185
|
+
self._part: str = part if part is not None else ""
|
|
186
|
+
|
|
187
|
+
def __repr__(self) -> str:
|
|
188
|
+
"""Return human-readable representation."""
|
|
189
|
+
return f'<TopicPartExact>(topic="{self.part}")'
|
|
190
|
+
|
|
191
|
+
def __len__(self) -> int:
|
|
192
|
+
"""Return the length of the stored literal in bytes."""
|
|
193
|
+
return len(self._part.encode('utf-8'))
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def part(self) -> str:
|
|
197
|
+
"""The literal string value for this part."""
|
|
198
|
+
return self._part
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class PyTopicPartAny(PyTopicPart):
|
|
202
|
+
"""Topic part representing a named wildcard."""
|
|
203
|
+
|
|
204
|
+
__slots__ = ('_name',)
|
|
205
|
+
|
|
206
|
+
def __init__(self, name: str = None, *args: Any, alloc: bool = False, **kwargs: Any) -> None:
|
|
207
|
+
"""Create an 'any' topic part.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
name: Optional name for the wildcard.
|
|
211
|
+
alloc: If True, allocate underlying structures.
|
|
212
|
+
"""
|
|
213
|
+
super().__init__(*args, alloc=alloc, **kwargs)
|
|
214
|
+
self._ttype = PyTopicType.TOPIC_PART_ANY
|
|
215
|
+
self._name: str = name if name is not None else ""
|
|
216
|
+
|
|
217
|
+
def __repr__(self) -> str:
|
|
218
|
+
"""Return human-readable representation."""
|
|
219
|
+
return f'<TopicPartAny>(name="{self.name}")'
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def name(self) -> str:
|
|
223
|
+
"""The wildcard name (identifier)."""
|
|
224
|
+
return self._name
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class PyTopicPartRange(PyTopicPart):
|
|
228
|
+
"""Topic part representing a range (choice) among multiple literals."""
|
|
229
|
+
|
|
230
|
+
__slots__ = ('_options',)
|
|
231
|
+
|
|
232
|
+
def __init__(self, options: list[str] = None, *args: Any, alloc: bool = False, **kwargs: Any) -> None:
|
|
233
|
+
"""Create a range part.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
options: List of literal option strings.
|
|
237
|
+
alloc: If True, allocate underlying structures.
|
|
238
|
+
"""
|
|
239
|
+
super().__init__(*args, alloc=alloc, **kwargs)
|
|
240
|
+
self._ttype = PyTopicType.TOPIC_PART_RANGE
|
|
241
|
+
self._options: list[str] = options if options is not None else []
|
|
242
|
+
|
|
243
|
+
def __repr__(self) -> str:
|
|
244
|
+
"""Return human-readable representation."""
|
|
245
|
+
return f'<TopicPartRange>(n={len(self._options)}, options={self._options})'
|
|
246
|
+
|
|
247
|
+
def __len__(self) -> int:
|
|
248
|
+
"""Return the number of options."""
|
|
249
|
+
return len(self._options)
|
|
250
|
+
|
|
251
|
+
def __iter__(self) -> Iterator[str]:
|
|
252
|
+
"""Iterate over option strings."""
|
|
253
|
+
return iter(self._options)
|
|
254
|
+
|
|
255
|
+
def options(self) -> Iterator[str]:
|
|
256
|
+
"""Yield option strings in order.
|
|
257
|
+
|
|
258
|
+
Yields:
|
|
259
|
+
Each option as a Python string.
|
|
260
|
+
"""
|
|
261
|
+
return iter(self._options)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class PyTopicPartPattern(PyTopicPart):
|
|
265
|
+
"""Topic part representing a regex pattern."""
|
|
266
|
+
|
|
267
|
+
__slots__ = ('_pattern', '_compiled_regex')
|
|
268
|
+
|
|
269
|
+
def __init__(self, regex: str = None, *args: Any, alloc: bool = False, **kwargs: Any) -> None:
|
|
270
|
+
"""Create a pattern topic part.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
regex: Regular expression string.
|
|
274
|
+
alloc: If True, allocate underlying structures.
|
|
275
|
+
"""
|
|
276
|
+
super().__init__(*args, alloc=alloc, **kwargs)
|
|
277
|
+
self._ttype = PyTopicType.TOPIC_PART_PATTERN
|
|
278
|
+
self._pattern: str = regex if regex is not None else ""
|
|
279
|
+
self._compiled_regex: re.Pattern | None = None
|
|
280
|
+
|
|
281
|
+
def __repr__(self) -> str:
|
|
282
|
+
"""Return human-readable representation."""
|
|
283
|
+
return f'<TopicPartPattern>(regex="{self.pattern}")'
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def pattern(self) -> str:
|
|
287
|
+
"""str: The raw regex pattern string."""
|
|
288
|
+
return self._pattern
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def regex(self) -> re.Pattern:
|
|
292
|
+
"""re.Pattern: Compiled regex object for the pattern."""
|
|
293
|
+
if self._compiled_regex is None:
|
|
294
|
+
self._compiled_regex = re.compile(self._pattern)
|
|
295
|
+
return self._compiled_regex
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class TopicMatchNode(TypedDict):
|
|
299
|
+
"""TypedDict describing a single match node returned by PyTopicMatchResult accessors.
|
|
300
|
+
|
|
301
|
+
Keys:
|
|
302
|
+
matched: Whether this node matched.
|
|
303
|
+
part_a: The left-side topic part (or None).
|
|
304
|
+
part_b: The right-side topic part (or None).
|
|
305
|
+
literal: The literal string associated with this node (if any).
|
|
306
|
+
"""
|
|
307
|
+
matched: bool
|
|
308
|
+
part_a: PyTopicPart | None
|
|
309
|
+
part_b: PyTopicPart | None
|
|
310
|
+
literal: str | None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class PyTopicMatchResult:
|
|
314
|
+
"""Container for topic part match results (linked list-like).
|
|
315
|
+
|
|
316
|
+
Provides iteration and indexing over match nodes and utilities to convert results.
|
|
317
|
+
|
|
318
|
+
The public API yields `TopicMatchNode` entries for per-node accessors.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
def __init__(self, n_parts: int = 0, alloc: bool = False, allocator: Any = None, **kwargs: Any) -> None:
|
|
322
|
+
"""Allocate or attach a chain of match result nodes.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
n_parts: Number of nodes to pre-create.
|
|
326
|
+
alloc: If True, allocate underlying structures.
|
|
327
|
+
allocator: Optional Allocator (ignored in pure Python implementation).
|
|
328
|
+
"""
|
|
329
|
+
self._nodes: deque[TopicMatchNode] = deque()
|
|
330
|
+
if alloc and n_parts > 0:
|
|
331
|
+
for _ in range(n_parts):
|
|
332
|
+
self._nodes.append({'matched': False, 'part_a': None, 'part_b': None, 'literal': None})
|
|
333
|
+
|
|
334
|
+
def __repr__(self) -> str:
|
|
335
|
+
"""Return a compact representation with success/failure and length."""
|
|
336
|
+
status = "success" if self.matched else "failed"
|
|
337
|
+
return f'<TopicPartMatchResult {status}>(nodes={self.length})'
|
|
338
|
+
|
|
339
|
+
def __bool__(self) -> bool:
|
|
340
|
+
"""True if all nodes matched."""
|
|
341
|
+
return self.matched
|
|
342
|
+
|
|
343
|
+
def __len__(self) -> int:
|
|
344
|
+
"""Return number of nodes in the result chain."""
|
|
345
|
+
return len(self._nodes)
|
|
346
|
+
|
|
347
|
+
def __getitem__(self, idx: int) -> TopicMatchNode:
|
|
348
|
+
"""Return a single node's info as a TopicMatchNode.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
idx: Index of the node (supports negative indexing).
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
A TopicMatchNode TypedDict containing 'matched', 'part_a', 'part_b', 'literal'.
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
IndexError: If idx is out of range.
|
|
358
|
+
"""
|
|
359
|
+
# deque doesn't support negative indexing directly, convert to list for that
|
|
360
|
+
nodes_list = list(self._nodes)
|
|
361
|
+
return nodes_list[idx]
|
|
362
|
+
|
|
363
|
+
def __iter__(self) -> Iterator[TopicMatchNode]:
|
|
364
|
+
"""Iterate over node info dicts in sequence."""
|
|
365
|
+
return iter(self._nodes)
|
|
366
|
+
|
|
367
|
+
def to_dict(self) -> dict[str, PyTopicPart]:
|
|
368
|
+
"""Convert match results into a dictionary mapping literal -> matched part.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
A mapping from literal string to the matched PyTopicPart.
|
|
372
|
+
"""
|
|
373
|
+
result = {}
|
|
374
|
+
for node in self._nodes:
|
|
375
|
+
if node['literal'] and node['part_b']:
|
|
376
|
+
result[node['literal']] = node['part_b']
|
|
377
|
+
return result
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def owner(self) -> bool:
|
|
381
|
+
"""bool: Whether this Python object owns the underlying memory (always True in native Python)."""
|
|
382
|
+
return True
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def length(self) -> int:
|
|
386
|
+
"""int: Number of nodes in the result chain."""
|
|
387
|
+
return len(self._nodes)
|
|
388
|
+
|
|
389
|
+
@cached_property
|
|
390
|
+
def matched(self) -> bool:
|
|
391
|
+
"""bool: True if every node in the chain reports matched == True."""
|
|
392
|
+
return all(node['matched'] for node in self._nodes) if self._nodes else False
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class PyTopic:
|
|
396
|
+
"""High-level Python representation of a parsed topic.
|
|
397
|
+
|
|
398
|
+
PyTopic instances internalize their literal content into a shared internal dict.
|
|
399
|
+
All topics created via the normal constructor are internalized into the global map and do not
|
|
400
|
+
own the underlying character storage (the buffer is bound to the global map).
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
__slots__ = ('_parts', '_value', '_hash', '_is_exact')
|
|
404
|
+
|
|
405
|
+
def __init__(self, topic: str = None, *args: Any, alloc: bool = True, allocator: Any = None, **kwargs: Any):
|
|
406
|
+
"""Create a PyTopic from a topic string.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
topic: Topic string to parse.
|
|
410
|
+
alloc: If True, allocate and initialize (default for normal usage).
|
|
411
|
+
allocator: Optional allocator (ignored in pure Python implementation).
|
|
412
|
+
"""
|
|
413
|
+
self._parts: list[PyTopicPart] = []
|
|
414
|
+
self._value: str = ""
|
|
415
|
+
self._hash: int = 0
|
|
416
|
+
self._is_exact: bool = True
|
|
417
|
+
|
|
418
|
+
if not alloc:
|
|
419
|
+
if topic:
|
|
420
|
+
raise RuntimeError('Can not assign topic string when uninitialized!')
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
if topic:
|
|
424
|
+
self._parse_topic(topic)
|
|
425
|
+
# Internalize into global map
|
|
426
|
+
global _GLOBAL_INTERNAL_MAP
|
|
427
|
+
if topic not in _GLOBAL_INTERNAL_MAP:
|
|
428
|
+
_GLOBAL_INTERNAL_MAP[topic] = self
|
|
429
|
+
self._value = topic
|
|
430
|
+
self._hash = hash(topic)
|
|
431
|
+
|
|
432
|
+
def _parse_topic(self, topic_str: str) -> None:
|
|
433
|
+
"""Parse a topic string into parts."""
|
|
434
|
+
if not topic_str:
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
parts = []
|
|
438
|
+
i = 0
|
|
439
|
+
current_part = ""
|
|
440
|
+
|
|
441
|
+
while i < len(topic_str):
|
|
442
|
+
char = topic_str[i]
|
|
443
|
+
|
|
444
|
+
# Check for pattern delimiter
|
|
445
|
+
if char == DEFAULT_PATTERN_DELIM:
|
|
446
|
+
# Find matching closing delimiter
|
|
447
|
+
j = i + 1
|
|
448
|
+
while j < len(topic_str) and topic_str[j] != DEFAULT_PATTERN_DELIM:
|
|
449
|
+
j += 1
|
|
450
|
+
if j < len(topic_str):
|
|
451
|
+
# Extract pattern
|
|
452
|
+
pattern = topic_str[i + 1:j]
|
|
453
|
+
parts.append(PyTopicPartPattern(pattern, alloc=True))
|
|
454
|
+
self._is_exact = False
|
|
455
|
+
i = j + 1
|
|
456
|
+
if i < len(topic_str) and topic_str[i] == DEFAULT_TOPIC_SEP:
|
|
457
|
+
i += 1
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
# Check for range (options)
|
|
461
|
+
elif char == DEFAULT_RANGE_BRACKETS[0]:
|
|
462
|
+
# Find matching closing bracket
|
|
463
|
+
j = i + 1
|
|
464
|
+
depth = 1
|
|
465
|
+
while j < len(topic_str) and depth > 0:
|
|
466
|
+
if topic_str[j] == DEFAULT_RANGE_BRACKETS[0]:
|
|
467
|
+
depth += 1
|
|
468
|
+
elif topic_str[j] == DEFAULT_RANGE_BRACKETS[1]:
|
|
469
|
+
depth -= 1
|
|
470
|
+
j += 1
|
|
471
|
+
if depth == 0:
|
|
472
|
+
# Extract options
|
|
473
|
+
options_str = topic_str[i + 1:j - 1]
|
|
474
|
+
options = [opt for opt in options_str.split(DEFAULT_OPTION_SEP) if opt]
|
|
475
|
+
parts.append(PyTopicPartRange(options, alloc=True))
|
|
476
|
+
self._is_exact = False
|
|
477
|
+
i = j
|
|
478
|
+
if i < len(topic_str) and topic_str[i] == DEFAULT_TOPIC_SEP:
|
|
479
|
+
i += 1
|
|
480
|
+
continue
|
|
481
|
+
|
|
482
|
+
# Check for wildcard with brackets {name}
|
|
483
|
+
elif char == DEFAULT_WILDCARD_BRACKETS[0]:
|
|
484
|
+
j = i + 1
|
|
485
|
+
while j < len(topic_str) and topic_str[j] != DEFAULT_WILDCARD_BRACKETS[1]:
|
|
486
|
+
j += 1
|
|
487
|
+
if j < len(topic_str):
|
|
488
|
+
name = topic_str[i + 1:j]
|
|
489
|
+
parts.append(PyTopicPartAny(name, alloc=True))
|
|
490
|
+
self._is_exact = False
|
|
491
|
+
i = j + 1
|
|
492
|
+
if i < len(topic_str) and topic_str[i] == DEFAULT_TOPIC_SEP:
|
|
493
|
+
i += 1
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
# Check for wildcard marker +
|
|
497
|
+
elif char == DEFAULT_WILDCARD_MARKER:
|
|
498
|
+
# Read the name after +
|
|
499
|
+
j = i + 1
|
|
500
|
+
name_start = j
|
|
501
|
+
while j < len(topic_str) and topic_str[j] != DEFAULT_TOPIC_SEP:
|
|
502
|
+
j += 1
|
|
503
|
+
name = topic_str[name_start:j]
|
|
504
|
+
parts.append(PyTopicPartAny(name, alloc=True))
|
|
505
|
+
self._is_exact = False
|
|
506
|
+
i = j
|
|
507
|
+
if i < len(topic_str) and topic_str[i] == DEFAULT_TOPIC_SEP:
|
|
508
|
+
i += 1
|
|
509
|
+
continue
|
|
510
|
+
|
|
511
|
+
# Regular part - read until separator
|
|
512
|
+
elif char == DEFAULT_TOPIC_SEP:
|
|
513
|
+
if current_part:
|
|
514
|
+
parts.append(PyTopicPartExact(current_part, alloc=True))
|
|
515
|
+
current_part = ""
|
|
516
|
+
i += 1
|
|
517
|
+
else:
|
|
518
|
+
current_part += char
|
|
519
|
+
i += 1
|
|
520
|
+
|
|
521
|
+
# Add final part
|
|
522
|
+
if current_part:
|
|
523
|
+
parts.append(PyTopicPartExact(current_part, alloc=True))
|
|
524
|
+
|
|
525
|
+
# Link parts
|
|
526
|
+
for j in range(len(parts) - 1):
|
|
527
|
+
parts[j]._next = parts[j + 1]
|
|
528
|
+
|
|
529
|
+
self._parts = parts
|
|
530
|
+
|
|
531
|
+
def _update_literal(self) -> None:
|
|
532
|
+
"""Reconstruct the literal value from parts."""
|
|
533
|
+
if not self._parts:
|
|
534
|
+
self._value = ""
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
parts_strs = []
|
|
538
|
+
for part in self._parts:
|
|
539
|
+
if isinstance(part, PyTopicPartExact):
|
|
540
|
+
parts_strs.append(part.part)
|
|
541
|
+
elif isinstance(part, PyTopicPartAny):
|
|
542
|
+
parts_strs.append(f"{DEFAULT_WILDCARD_BRACKETS[0]}{part.name}{DEFAULT_WILDCARD_BRACKETS[1]}")
|
|
543
|
+
elif isinstance(part, PyTopicPartRange):
|
|
544
|
+
options_str = DEFAULT_OPTION_SEP.join(part._options)
|
|
545
|
+
parts_strs.append(f"{DEFAULT_RANGE_BRACKETS[0]}{options_str}{DEFAULT_RANGE_BRACKETS[1]}")
|
|
546
|
+
elif isinstance(part, PyTopicPartPattern):
|
|
547
|
+
parts_strs.append(f"{DEFAULT_PATTERN_DELIM}{part.pattern}{DEFAULT_PATTERN_DELIM}")
|
|
548
|
+
|
|
549
|
+
self._value = DEFAULT_TOPIC_SEP.join(parts_strs)
|
|
550
|
+
self._hash = hash(self._value)
|
|
551
|
+
|
|
552
|
+
def __len__(self) -> int:
|
|
553
|
+
"""Return the number of parts in the topic."""
|
|
554
|
+
return len(self._parts)
|
|
555
|
+
|
|
556
|
+
def __iter__(self) -> Iterator[PyTopicPart]:
|
|
557
|
+
"""Iterate over topic parts (yields PyTopicPart subclasses)."""
|
|
558
|
+
return iter(self._parts)
|
|
559
|
+
|
|
560
|
+
def __getitem__(self, idx: int) -> PyTopicPart:
|
|
561
|
+
"""Return the topic part at index `idx`.
|
|
562
|
+
|
|
563
|
+
Supports negative indexing.
|
|
564
|
+
|
|
565
|
+
Raises:
|
|
566
|
+
IndexError: If index is out of range.
|
|
567
|
+
"""
|
|
568
|
+
return self._parts[idx]
|
|
569
|
+
|
|
570
|
+
@overload
|
|
571
|
+
def __add__(self, topic: 'PyTopic') -> 'PyTopic':
|
|
572
|
+
...
|
|
573
|
+
|
|
574
|
+
@overload
|
|
575
|
+
def __add__(self, topic: PyTopicPart) -> 'PyTopic':
|
|
576
|
+
...
|
|
577
|
+
|
|
578
|
+
def __add__(self, topic: 'PyTopic | PyTopicPart') -> 'PyTopic':
|
|
579
|
+
"""Return a new PyTopic that aggregates this topic with another PyTopic or PyTopicPart.
|
|
580
|
+
|
|
581
|
+
Behavior:
|
|
582
|
+
- `__add__` creates and returns a copy; both operands remain unchanged.
|
|
583
|
+
- Use `append` or `__iadd__` for in-place modifications.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
topic: Either a PyTopic or PyTopicPart.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
A new aggregated PyTopic.
|
|
590
|
+
|
|
591
|
+
Raises:
|
|
592
|
+
TypeError: When the operand type is unsupported.
|
|
593
|
+
"""
|
|
594
|
+
new_topic = PyTopic.__new__(PyTopic)
|
|
595
|
+
new_topic._parts = list(self._parts)
|
|
596
|
+
new_topic._is_exact = self._is_exact
|
|
597
|
+
|
|
598
|
+
if isinstance(topic, PyTopic):
|
|
599
|
+
new_topic._parts.extend(topic._parts)
|
|
600
|
+
if not topic._is_exact:
|
|
601
|
+
new_topic._is_exact = False
|
|
602
|
+
elif isinstance(topic, PyTopicPart):
|
|
603
|
+
new_topic._parts.append(topic)
|
|
604
|
+
if topic.ttype != PyTopicType.TOPIC_PART_EXACT:
|
|
605
|
+
new_topic._is_exact = False
|
|
606
|
+
else:
|
|
607
|
+
raise TypeError(f'Can not add {topic} to {self}, expected either a PyTopic or PyTopicPart')
|
|
608
|
+
|
|
609
|
+
new_topic._update_literal()
|
|
610
|
+
return new_topic
|
|
611
|
+
|
|
612
|
+
@overload
|
|
613
|
+
def __iadd__(self, topic: 'PyTopic') -> 'PyTopic':
|
|
614
|
+
...
|
|
615
|
+
|
|
616
|
+
@overload
|
|
617
|
+
def __iadd__(self, topic: PyTopicPart) -> 'PyTopic':
|
|
618
|
+
...
|
|
619
|
+
|
|
620
|
+
def __iadd__(self, topic: 'PyTopic | PyTopicPart') -> 'PyTopic':
|
|
621
|
+
"""In-place append another PyTopic or PyTopicPart.
|
|
622
|
+
|
|
623
|
+
Behavior:
|
|
624
|
+
- Modifies `self` in place and leaves the other operand unchanged.
|
|
625
|
+
- This is equivalent to `self.append(...)` and returns `self`.
|
|
626
|
+
"""
|
|
627
|
+
if isinstance(topic, PyTopic):
|
|
628
|
+
self._parts.extend(topic._parts)
|
|
629
|
+
if not topic._is_exact:
|
|
630
|
+
self._is_exact = False
|
|
631
|
+
elif isinstance(topic, PyTopicPart):
|
|
632
|
+
self._parts.append(topic)
|
|
633
|
+
if topic.ttype != PyTopicType.TOPIC_PART_EXACT:
|
|
634
|
+
self._is_exact = False
|
|
635
|
+
else:
|
|
636
|
+
raise TypeError(f'Can not add {topic} to {self}, expected either a PyTopic or PyTopicPart')
|
|
637
|
+
|
|
638
|
+
self._update_literal()
|
|
639
|
+
return self
|
|
640
|
+
|
|
641
|
+
def __hash__(self):
|
|
642
|
+
"""Return hash based on the topic's literal value.
|
|
643
|
+
|
|
644
|
+
The hash is precomputed during initialization for efficiency.
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
A uint64_t hash integer.
|
|
648
|
+
"""
|
|
649
|
+
return self._hash
|
|
650
|
+
|
|
651
|
+
def __eq__(self, other: 'PyTopic') -> bool:
|
|
652
|
+
"""Check equality between this topic and another topic.
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
other: The other PyTopic to compare against.
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
True if both topics have the same literal value.
|
|
659
|
+
"""
|
|
660
|
+
if not isinstance(other, PyTopic):
|
|
661
|
+
return False
|
|
662
|
+
return self._value == other._value
|
|
663
|
+
|
|
664
|
+
def __repr__(self) -> str:
|
|
665
|
+
"""Return string representation."""
|
|
666
|
+
exact_or_generic = "Exact" if self._is_exact else "Generic"
|
|
667
|
+
return f'<{self.__class__.__name__} {exact_or_generic}>(value="{self._value}", n_parts={len(self._parts)})'
|
|
668
|
+
|
|
669
|
+
def __str__(self) -> str:
|
|
670
|
+
"""Return the topic literal value."""
|
|
671
|
+
return self._value
|
|
672
|
+
|
|
673
|
+
def __call__(self, **kwargs) -> 'PyTopic':
|
|
674
|
+
"""Alias of ``format`` method to format the topic by replacing named wildcards with provided values."""
|
|
675
|
+
return self.format_map(kwargs, internalized=True, strict=False)
|
|
676
|
+
|
|
677
|
+
@classmethod
|
|
678
|
+
def from_parts(cls, topic_parts: Iterable[PyTopicPart]) -> PyTopic:
|
|
679
|
+
"""Build a PyTopic from an iterable of PyTopicPart instances."""
|
|
680
|
+
new_topic = cls.__new__(cls)
|
|
681
|
+
new_topic._parts = list(topic_parts)
|
|
682
|
+
new_topic._is_exact = all(p.ttype == PyTopicType.TOPIC_PART_EXACT for p in new_topic._parts)
|
|
683
|
+
new_topic._update_literal()
|
|
684
|
+
return new_topic
|
|
685
|
+
|
|
686
|
+
@classmethod
|
|
687
|
+
def join(cls, topic_parts: Iterable[str]) -> 'PyTopic':
|
|
688
|
+
"""Build a PyTopic from an iterable of literal strings.
|
|
689
|
+
|
|
690
|
+
Each string is appended as an exact part.
|
|
691
|
+
|
|
692
|
+
Notes:
|
|
693
|
+
- This is a higher-level helper for simple literal-only topics.
|
|
694
|
+
- For complex parts that need escaping or patterns, use `from_parts`.
|
|
695
|
+
"""
|
|
696
|
+
parts = [PyTopicPartExact(part, alloc=True) for part in topic_parts]
|
|
697
|
+
return cls.from_parts(parts)
|
|
698
|
+
|
|
699
|
+
def append(self, topic_part: PyTopicPart) -> 'PyTopic':
|
|
700
|
+
"""Append a PyTopicPart to this topic (high-level API).
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
topic_part: The part to append.
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
Self for chaining.
|
|
707
|
+
|
|
708
|
+
Raises:
|
|
709
|
+
RuntimeError: If either topic or part is uninitialized.
|
|
710
|
+
"""
|
|
711
|
+
self._parts.append(topic_part)
|
|
712
|
+
if topic_part.ttype != PyTopicType.TOPIC_PART_EXACT:
|
|
713
|
+
self._is_exact = False
|
|
714
|
+
self._update_literal()
|
|
715
|
+
return self
|
|
716
|
+
|
|
717
|
+
def match(self, other: 'PyTopic') -> PyTopicMatchResult:
|
|
718
|
+
"""Match this topic against another topic.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
other: The topic to match against.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
A PyTopicMatchResult describing per-part matches.
|
|
725
|
+
"""
|
|
726
|
+
result = PyTopicMatchResult(alloc=True)
|
|
727
|
+
|
|
728
|
+
# Simple matching logic: compare parts
|
|
729
|
+
self_parts = list(self._parts)
|
|
730
|
+
other_parts = list(other._parts)
|
|
731
|
+
|
|
732
|
+
max_len = max(len(self_parts), len(other_parts))
|
|
733
|
+
|
|
734
|
+
for i in range(max_len):
|
|
735
|
+
node: TopicMatchNode = {
|
|
736
|
+
'matched': False,
|
|
737
|
+
'part_a': None,
|
|
738
|
+
'part_b': None,
|
|
739
|
+
'literal': None
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if i >= len(self_parts) or i >= len(other_parts):
|
|
743
|
+
# Length mismatch
|
|
744
|
+
result._nodes.append(node)
|
|
745
|
+
continue
|
|
746
|
+
|
|
747
|
+
part_a = self_parts[i]
|
|
748
|
+
part_b = other_parts[i]
|
|
749
|
+
node['part_a'] = part_a
|
|
750
|
+
node['part_b'] = part_b
|
|
751
|
+
|
|
752
|
+
# Match logic
|
|
753
|
+
if part_a.ttype == PyTopicType.TOPIC_PART_EXACT and part_b.ttype == PyTopicType.TOPIC_PART_EXACT:
|
|
754
|
+
# Exact match - cast to PyTopicPartExact
|
|
755
|
+
part_a_exact = part_a if isinstance(part_a, PyTopicPartExact) else None
|
|
756
|
+
part_b_exact = part_b if isinstance(part_b, PyTopicPartExact) else None
|
|
757
|
+
if part_a_exact and part_b_exact and part_a_exact.part == part_b_exact.part:
|
|
758
|
+
node['matched'] = True
|
|
759
|
+
node['literal'] = part_a_exact.part
|
|
760
|
+
elif part_a.ttype == PyTopicType.TOPIC_PART_ANY:
|
|
761
|
+
# Wildcard matches anything
|
|
762
|
+
node['matched'] = True
|
|
763
|
+
if part_b.ttype == PyTopicType.TOPIC_PART_EXACT:
|
|
764
|
+
part_b_exact = part_b if isinstance(part_b, PyTopicPartExact) else None
|
|
765
|
+
if part_b_exact:
|
|
766
|
+
node['literal'] = part_b_exact.part
|
|
767
|
+
elif part_b.ttype == PyTopicType.TOPIC_PART_ANY:
|
|
768
|
+
# Wildcard matches anything
|
|
769
|
+
node['matched'] = True
|
|
770
|
+
if part_a.ttype == PyTopicType.TOPIC_PART_EXACT:
|
|
771
|
+
part_a_exact = part_a if isinstance(part_a, PyTopicPartExact) else None
|
|
772
|
+
if part_a_exact:
|
|
773
|
+
node['literal'] = part_a_exact.part
|
|
774
|
+
elif part_a.ttype == PyTopicType.TOPIC_PART_RANGE:
|
|
775
|
+
# Range match - cast to PyTopicPartRange
|
|
776
|
+
if part_b.ttype == PyTopicType.TOPIC_PART_EXACT:
|
|
777
|
+
part_a_range = part_a if isinstance(part_a, PyTopicPartRange) else None
|
|
778
|
+
part_b_exact = part_b if isinstance(part_b, PyTopicPartExact) else None
|
|
779
|
+
if part_a_range and part_b_exact and part_b_exact.part in part_a_range._options:
|
|
780
|
+
node['matched'] = True
|
|
781
|
+
node['literal'] = part_b_exact.part
|
|
782
|
+
elif part_b.ttype == PyTopicType.TOPIC_PART_RANGE:
|
|
783
|
+
# Range match (reversed) - cast to PyTopicPartRange
|
|
784
|
+
if part_a.ttype == PyTopicType.TOPIC_PART_EXACT:
|
|
785
|
+
part_a_exact = part_a if isinstance(part_a, PyTopicPartExact) else None
|
|
786
|
+
part_b_range = part_b if isinstance(part_b, PyTopicPartRange) else None
|
|
787
|
+
if part_a_exact and part_b_range and part_a_exact.part in part_b_range._options:
|
|
788
|
+
node['matched'] = True
|
|
789
|
+
node['literal'] = part_a_exact.part
|
|
790
|
+
elif part_a.ttype == PyTopicType.TOPIC_PART_PATTERN:
|
|
791
|
+
# Pattern match - cast to PyTopicPartPattern
|
|
792
|
+
if part_b.ttype == PyTopicType.TOPIC_PART_EXACT:
|
|
793
|
+
part_a_pattern = part_a if isinstance(part_a, PyTopicPartPattern) else None
|
|
794
|
+
part_b_exact = part_b if isinstance(part_b, PyTopicPartExact) else None
|
|
795
|
+
if part_a_pattern and part_b_exact and part_a_pattern.regex.match(part_b_exact.part):
|
|
796
|
+
node['matched'] = True
|
|
797
|
+
node['literal'] = part_b_exact.part
|
|
798
|
+
elif part_b.ttype == PyTopicType.TOPIC_PART_PATTERN:
|
|
799
|
+
# Pattern match (reversed) - cast to PyTopicPartPattern
|
|
800
|
+
if part_a.ttype == PyTopicType.TOPIC_PART_EXACT:
|
|
801
|
+
part_a_exact = part_a if isinstance(part_a, PyTopicPartExact) else None
|
|
802
|
+
part_b_pattern = part_b if isinstance(part_b, PyTopicPartPattern) else None
|
|
803
|
+
if part_a_exact and part_b_pattern and part_b_pattern.regex.match(part_a_exact.part):
|
|
804
|
+
node['matched'] = True
|
|
805
|
+
node['literal'] = part_a_exact.part
|
|
806
|
+
|
|
807
|
+
result._nodes.append(node)
|
|
808
|
+
|
|
809
|
+
return result
|
|
810
|
+
|
|
811
|
+
def update_literal(self) -> 'PyTopic':
|
|
812
|
+
"""Update the internal literal buffer to reflect the current parts.
|
|
813
|
+
|
|
814
|
+
This is useful after in-place modifications of the subordinate TopicParts.
|
|
815
|
+
To avoid inconsistencies, call this method to regenerate the internal literal.
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
Self for chaining.
|
|
819
|
+
|
|
820
|
+
Raises:
|
|
821
|
+
RuntimeError: If the topic is uninitialized.
|
|
822
|
+
"""
|
|
823
|
+
self._update_literal()
|
|
824
|
+
return self
|
|
825
|
+
|
|
826
|
+
def format(self, **kwargs) -> PyTopic:
|
|
827
|
+
"""Format the topic by replacing named wildcards with provided values.
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
**kwargs: Mapping from wildcard names to replacement strings.
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
A new Exact PyTopic with wildcards replaced by the provided values.
|
|
834
|
+
|
|
835
|
+
Raises:
|
|
836
|
+
KeyError: If a required wildcard name is missing in kwargs.
|
|
837
|
+
ValueError: If having subordinate parts that are not exact or wildcards.
|
|
838
|
+
"""
|
|
839
|
+
return self.format_map(kwargs, internalized=True, strict=False)
|
|
840
|
+
|
|
841
|
+
def format_map(self, mapping: dict[str, str], internalized: bool = True, strict: bool = False) -> PyTopic:
|
|
842
|
+
"""Format the topic by replacing named wildcards with provided values from a mapping.
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
mapping: Dictionary mapping wildcard names to replacement strings.
|
|
846
|
+
internalized: If True, the returned topic is internalized into the global map and not owning its underlying buffer.
|
|
847
|
+
If False, the returned topic owns its internal buffer.
|
|
848
|
+
strict: If True, raises KeyError if a wildcard name is missing in mapping.
|
|
849
|
+
|
|
850
|
+
Returns:
|
|
851
|
+
A new PyTopic with wildcards replaced by the provided values. If strict is True, the new PyTopic is guaranteed to be an exact PyTopic.
|
|
852
|
+
|
|
853
|
+
Raises:
|
|
854
|
+
KeyError: If a required wildcard name is missing in mapping.
|
|
855
|
+
ValueError: If having subordinate parts that are not exact or wildcards.
|
|
856
|
+
"""
|
|
857
|
+
new_parts = []
|
|
858
|
+
|
|
859
|
+
for part in self._parts:
|
|
860
|
+
if part.ttype == PyTopicType.TOPIC_PART_EXACT:
|
|
861
|
+
part_exact = part if isinstance(part, PyTopicPartExact) else None
|
|
862
|
+
if part_exact:
|
|
863
|
+
new_parts.append(PyTopicPartExact(part_exact.part, alloc=True))
|
|
864
|
+
elif part.ttype == PyTopicType.TOPIC_PART_ANY:
|
|
865
|
+
part_any = part if isinstance(part, PyTopicPartAny) else None
|
|
866
|
+
if part_any:
|
|
867
|
+
if part_any.name not in mapping:
|
|
868
|
+
if strict:
|
|
869
|
+
raise KeyError(part_any.name)
|
|
870
|
+
else:
|
|
871
|
+
new_parts.append(PyTopicPartAny(part_any.name, alloc=True))
|
|
872
|
+
else:
|
|
873
|
+
new_parts.append(PyTopicPartExact(mapping[part_any.name], alloc=True))
|
|
874
|
+
else:
|
|
875
|
+
raise ValueError(f'Not supported topic type {part.ttype}')
|
|
876
|
+
|
|
877
|
+
new_topic = PyTopic.from_parts(new_parts)
|
|
878
|
+
|
|
879
|
+
if internalized:
|
|
880
|
+
global _GLOBAL_INTERNAL_MAP
|
|
881
|
+
if new_topic._value not in _GLOBAL_INTERNAL_MAP:
|
|
882
|
+
_GLOBAL_INTERNAL_MAP[new_topic._value] = new_topic
|
|
883
|
+
|
|
884
|
+
return new_topic
|
|
885
|
+
|
|
886
|
+
@property
|
|
887
|
+
def value(self) -> str:
|
|
888
|
+
"""str: The full topic literal value.
|
|
889
|
+
|
|
890
|
+
Getting returns the current literal. Setting attempts to assign and will raise
|
|
891
|
+
ValueError on syntax errors. Setting mutates the internalized buffer and will
|
|
892
|
+
re-register the topic in the internal mapping.
|
|
893
|
+
"""
|
|
894
|
+
return self._value
|
|
895
|
+
|
|
896
|
+
@value.setter
|
|
897
|
+
def value(self, value: str) -> None:
|
|
898
|
+
"""Set the topic literal value.
|
|
899
|
+
|
|
900
|
+
Args:
|
|
901
|
+
value: New topic string to assign.
|
|
902
|
+
|
|
903
|
+
Raises:
|
|
904
|
+
ValueError: If assignment fails due to syntax error.
|
|
905
|
+
|
|
906
|
+
Notes:
|
|
907
|
+
- Mutating `value` is an expensive operation (de-register / re-register). Avoid frequent calls.
|
|
908
|
+
- Prefer lazy-init: create an empty topic and set `value` once when needed.
|
|
909
|
+
"""
|
|
910
|
+
# Remove from internal map if present
|
|
911
|
+
global _GLOBAL_INTERNAL_MAP
|
|
912
|
+
if self._value in _GLOBAL_INTERNAL_MAP and _GLOBAL_INTERNAL_MAP[self._value] is self:
|
|
913
|
+
del _GLOBAL_INTERNAL_MAP[self._value]
|
|
914
|
+
|
|
915
|
+
# Parse new value
|
|
916
|
+
self._parts = []
|
|
917
|
+
self._value = ""
|
|
918
|
+
self._hash = 0
|
|
919
|
+
self._is_exact = True
|
|
920
|
+
|
|
921
|
+
try:
|
|
922
|
+
self._parse_topic(value)
|
|
923
|
+
self._value = value
|
|
924
|
+
self._hash = hash(value)
|
|
925
|
+
|
|
926
|
+
# Re-internalize
|
|
927
|
+
if value not in _GLOBAL_INTERNAL_MAP:
|
|
928
|
+
_GLOBAL_INTERNAL_MAP[value] = self
|
|
929
|
+
except Exception as e:
|
|
930
|
+
raise ValueError(f'Failed to assign topic "{value}", check if syntax is correct!') from e
|
|
931
|
+
|
|
932
|
+
@property
|
|
933
|
+
def owner(self) -> bool:
|
|
934
|
+
"""bool: Whether this Python object owns the underlying memory (always True in native Python)."""
|
|
935
|
+
return True
|
|
936
|
+
|
|
937
|
+
@property
|
|
938
|
+
def is_exact(self) -> bool:
|
|
939
|
+
"""bool: True if the topic consists only of exact parts (no wildcards, ranges, or patterns)."""
|
|
940
|
+
return self._is_exact
|
|
941
|
+
|
|
942
|
+
@property
|
|
943
|
+
def addr(self) -> int:
|
|
944
|
+
"""int: Numeric address / id of the underlying structure."""
|
|
945
|
+
return id(self)
|