libmorpheus 0.1.0__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.
- libmorpheus/__init__.py +65 -0
- libmorpheus/_abi.py +227 -0
- libmorpheus/_library.py +332 -0
- libmorpheus/_types.py +502 -0
- libmorpheus/_version.py +6 -0
- libmorpheus/py.typed +0 -0
- libmorpheus-0.1.0.dist-info/METADATA +398 -0
- libmorpheus-0.1.0.dist-info/RECORD +12 -0
- libmorpheus-0.1.0.dist-info/WHEEL +5 -0
- libmorpheus-0.1.0.dist-info/licenses/LICENSE +661 -0
- libmorpheus-0.1.0.dist-info/licenses/NOTICE +12 -0
- libmorpheus-0.1.0.dist-info/top_level.txt +1 -0
libmorpheus/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Typed Python binding for the libmorpheus native runtime."""
|
|
3
|
+
|
|
4
|
+
from ._library import Context, Library, MorpheusError
|
|
5
|
+
from ._types import (
|
|
6
|
+
Analysis,
|
|
7
|
+
Case,
|
|
8
|
+
Degree,
|
|
9
|
+
Dialect,
|
|
10
|
+
Gender,
|
|
11
|
+
Generation,
|
|
12
|
+
GenerationOptions,
|
|
13
|
+
GeographicRegion,
|
|
14
|
+
Language,
|
|
15
|
+
Mood,
|
|
16
|
+
Number,
|
|
17
|
+
Option,
|
|
18
|
+
PartOfSpeech,
|
|
19
|
+
Person,
|
|
20
|
+
RawAnalysis,
|
|
21
|
+
RawGeneration,
|
|
22
|
+
Status,
|
|
23
|
+
Tense,
|
|
24
|
+
TruncatedField,
|
|
25
|
+
Voice,
|
|
26
|
+
has_morph_flag,
|
|
27
|
+
)
|
|
28
|
+
from ._version import (
|
|
29
|
+
MORPHEUS_NATIVE_ABI_VERSION,
|
|
30
|
+
MORPHEUS_NATIVE_VERSION,
|
|
31
|
+
MORPHEUS_PYTHON_VERSION,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__version__ = MORPHEUS_PYTHON_VERSION
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"Analysis",
|
|
38
|
+
"Case",
|
|
39
|
+
"Context",
|
|
40
|
+
"Degree",
|
|
41
|
+
"Dialect",
|
|
42
|
+
"Gender",
|
|
43
|
+
"Generation",
|
|
44
|
+
"GenerationOptions",
|
|
45
|
+
"GeographicRegion",
|
|
46
|
+
"Language",
|
|
47
|
+
"Library",
|
|
48
|
+
"Mood",
|
|
49
|
+
"MORPHEUS_NATIVE_ABI_VERSION",
|
|
50
|
+
"MORPHEUS_NATIVE_VERSION",
|
|
51
|
+
"MORPHEUS_PYTHON_VERSION",
|
|
52
|
+
"MorpheusError",
|
|
53
|
+
"Number",
|
|
54
|
+
"Option",
|
|
55
|
+
"PartOfSpeech",
|
|
56
|
+
"Person",
|
|
57
|
+
"RawAnalysis",
|
|
58
|
+
"RawGeneration",
|
|
59
|
+
"Status",
|
|
60
|
+
"Tense",
|
|
61
|
+
"TruncatedField",
|
|
62
|
+
"Voice",
|
|
63
|
+
"__version__",
|
|
64
|
+
"has_morph_flag",
|
|
65
|
+
]
|
libmorpheus/_abi.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Private ctypes declarations for libmorpheus ABI version 2."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ctypes
|
|
7
|
+
from os import PathLike, fspath
|
|
8
|
+
|
|
9
|
+
from ._version import MORPHEUS_NATIVE_ABI_VERSION
|
|
10
|
+
|
|
11
|
+
ABI_VERSION = MORPHEUS_NATIVE_ABI_VERSION
|
|
12
|
+
TEXT_CAPACITY = 64
|
|
13
|
+
DOMAIN_CAPACITY = 24
|
|
14
|
+
MORPH_FLAG_CAPACITY = 11
|
|
15
|
+
GENERATION_OPTIONS_VERSION = 1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AnalysisRecord(ctypes.Structure):
|
|
19
|
+
_fields_ = [
|
|
20
|
+
("struct_size", ctypes.c_uint32),
|
|
21
|
+
("part_of_speech", ctypes.c_uint32),
|
|
22
|
+
("dialect", ctypes.c_uint32),
|
|
23
|
+
("geographic_region", ctypes.c_uint32),
|
|
24
|
+
("person", ctypes.c_uint32),
|
|
25
|
+
("number", ctypes.c_uint32),
|
|
26
|
+
("gender", ctypes.c_uint32),
|
|
27
|
+
("grammatical_case", ctypes.c_uint32),
|
|
28
|
+
("tense", ctypes.c_uint32),
|
|
29
|
+
("mood", ctypes.c_uint32),
|
|
30
|
+
("voice", ctypes.c_uint32),
|
|
31
|
+
("degree", ctypes.c_uint32),
|
|
32
|
+
("raw", ctypes.c_char * TEXT_CAPACITY),
|
|
33
|
+
("workword", ctypes.c_char * TEXT_CAPACITY),
|
|
34
|
+
("lemma", ctypes.c_char * TEXT_CAPACITY),
|
|
35
|
+
("preverb", ctypes.c_char * TEXT_CAPACITY),
|
|
36
|
+
("augment", ctypes.c_char * TEXT_CAPACITY),
|
|
37
|
+
("stem", ctypes.c_char * TEXT_CAPACITY),
|
|
38
|
+
("suffix", ctypes.c_char * TEXT_CAPACITY),
|
|
39
|
+
("ending", ctypes.c_char * TEXT_CAPACITY),
|
|
40
|
+
("crasis", ctypes.c_char * TEXT_CAPACITY),
|
|
41
|
+
("dictionary_form", ctypes.c_char * TEXT_CAPACITY),
|
|
42
|
+
("english_form", ctypes.c_char * TEXT_CAPACITY),
|
|
43
|
+
("raw_preverb", ctypes.c_char * TEXT_CAPACITY),
|
|
44
|
+
("domains", ctypes.c_char * DOMAIN_CAPACITY),
|
|
45
|
+
("morph_flags", ctypes.c_uint8 * MORPH_FLAG_CAPACITY),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GenerationRecord(ctypes.Structure):
|
|
50
|
+
_fields_ = [
|
|
51
|
+
("struct_size", ctypes.c_uint32),
|
|
52
|
+
("part_of_speech", ctypes.c_uint32),
|
|
53
|
+
("dialect", ctypes.c_uint32),
|
|
54
|
+
("geographic_region", ctypes.c_uint32),
|
|
55
|
+
("person", ctypes.c_uint32),
|
|
56
|
+
("number", ctypes.c_uint32),
|
|
57
|
+
("gender", ctypes.c_uint32),
|
|
58
|
+
("grammatical_case", ctypes.c_uint32),
|
|
59
|
+
("tense", ctypes.c_uint32),
|
|
60
|
+
("mood", ctypes.c_uint32),
|
|
61
|
+
("voice", ctypes.c_uint32),
|
|
62
|
+
("degree", ctypes.c_uint32),
|
|
63
|
+
("surface", ctypes.c_char * TEXT_CAPACITY),
|
|
64
|
+
("lemma", ctypes.c_char * TEXT_CAPACITY),
|
|
65
|
+
("morph_flags", ctypes.c_uint8 * MORPH_FLAG_CAPACITY),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class NativeGenerationOptions(ctypes.Structure):
|
|
70
|
+
_fields_ = [
|
|
71
|
+
("version", ctypes.c_uint32),
|
|
72
|
+
("struct_size", ctypes.c_uint32),
|
|
73
|
+
("result_limit", ctypes.c_uint64),
|
|
74
|
+
("flags", ctypes.c_uint32),
|
|
75
|
+
("part_of_speech", ctypes.c_uint32),
|
|
76
|
+
("dialect", ctypes.c_uint32),
|
|
77
|
+
("geographic_region", ctypes.c_uint32),
|
|
78
|
+
("person", ctypes.c_uint32),
|
|
79
|
+
("number", ctypes.c_uint32),
|
|
80
|
+
("gender", ctypes.c_uint32),
|
|
81
|
+
("grammatical_case", ctypes.c_uint32),
|
|
82
|
+
("tense", ctypes.c_uint32),
|
|
83
|
+
("mood", ctypes.c_uint32),
|
|
84
|
+
("voice", ctypes.c_uint32),
|
|
85
|
+
("degree", ctypes.c_uint32),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _function(library: ctypes.CDLL, name: str, restype: object, *argtypes: object):
|
|
90
|
+
try:
|
|
91
|
+
function = getattr(library, name)
|
|
92
|
+
except AttributeError as error:
|
|
93
|
+
raise RuntimeError(f"Missing native symbol: {name}") from error
|
|
94
|
+
function.restype = restype
|
|
95
|
+
function.argtypes = list(argtypes)
|
|
96
|
+
return function
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class NativeLibrary:
|
|
100
|
+
"""Validated ctypes function table for one native shared library."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, path: str | PathLike[str]) -> None:
|
|
103
|
+
self.path = fspath(path)
|
|
104
|
+
self.handle = ctypes.CDLL(self.path)
|
|
105
|
+
self.abi_version = _function(
|
|
106
|
+
self.handle, "morpheus_abi_version", ctypes.c_uint32
|
|
107
|
+
)
|
|
108
|
+
self.analysis_size = _function(
|
|
109
|
+
self.handle, "morpheus_analysis_size", ctypes.c_size_t
|
|
110
|
+
)
|
|
111
|
+
self.generation_size = _function(
|
|
112
|
+
self.handle, "morpheus_generation_size", ctypes.c_size_t
|
|
113
|
+
)
|
|
114
|
+
self.status_message = _function(
|
|
115
|
+
self.handle,
|
|
116
|
+
"morpheus_status_message",
|
|
117
|
+
ctypes.c_char_p,
|
|
118
|
+
ctypes.c_uint32,
|
|
119
|
+
)
|
|
120
|
+
self.open_path = _function(
|
|
121
|
+
self.handle,
|
|
122
|
+
"morpheus_open_path",
|
|
123
|
+
ctypes.c_uint32,
|
|
124
|
+
ctypes.c_uint32,
|
|
125
|
+
ctypes.POINTER(ctypes.c_uint8),
|
|
126
|
+
ctypes.c_size_t,
|
|
127
|
+
ctypes.c_uint32,
|
|
128
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
129
|
+
)
|
|
130
|
+
self.close_context = _function(
|
|
131
|
+
self.handle, "morpheus_close", None, ctypes.c_void_p
|
|
132
|
+
)
|
|
133
|
+
self.analyze = _function(
|
|
134
|
+
self.handle,
|
|
135
|
+
"morpheus_analyze",
|
|
136
|
+
ctypes.c_uint32,
|
|
137
|
+
ctypes.c_void_p,
|
|
138
|
+
ctypes.POINTER(ctypes.c_uint8),
|
|
139
|
+
ctypes.c_size_t,
|
|
140
|
+
ctypes.c_uint64,
|
|
141
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
142
|
+
)
|
|
143
|
+
self.result_count = _function(
|
|
144
|
+
self.handle, "morpheus_result_count", ctypes.c_size_t, ctypes.c_void_p
|
|
145
|
+
)
|
|
146
|
+
self.result_copy = _function(
|
|
147
|
+
self.handle,
|
|
148
|
+
"morpheus_result_copy",
|
|
149
|
+
ctypes.c_uint32,
|
|
150
|
+
ctypes.c_void_p,
|
|
151
|
+
ctypes.c_size_t,
|
|
152
|
+
ctypes.c_void_p,
|
|
153
|
+
ctypes.c_size_t,
|
|
154
|
+
)
|
|
155
|
+
self.result_truncated_fields = _function(
|
|
156
|
+
self.handle,
|
|
157
|
+
"morpheus_result_truncated_fields",
|
|
158
|
+
ctypes.c_uint32,
|
|
159
|
+
ctypes.c_void_p,
|
|
160
|
+
ctypes.c_size_t,
|
|
161
|
+
ctypes.POINTER(ctypes.c_uint32),
|
|
162
|
+
)
|
|
163
|
+
self.result_free = _function(
|
|
164
|
+
self.handle, "morpheus_result_free", None, ctypes.c_void_p
|
|
165
|
+
)
|
|
166
|
+
self.generate = _function(
|
|
167
|
+
self.handle,
|
|
168
|
+
"morpheus_generate",
|
|
169
|
+
ctypes.c_uint32,
|
|
170
|
+
ctypes.c_void_p,
|
|
171
|
+
ctypes.POINTER(ctypes.c_uint8),
|
|
172
|
+
ctypes.c_size_t,
|
|
173
|
+
ctypes.POINTER(NativeGenerationOptions),
|
|
174
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
175
|
+
)
|
|
176
|
+
self.generation_result_count = _function(
|
|
177
|
+
self.handle,
|
|
178
|
+
"morpheus_generation_result_count",
|
|
179
|
+
ctypes.c_size_t,
|
|
180
|
+
ctypes.c_void_p,
|
|
181
|
+
)
|
|
182
|
+
self.generation_result_copy = _function(
|
|
183
|
+
self.handle,
|
|
184
|
+
"morpheus_generation_result_copy",
|
|
185
|
+
ctypes.c_uint32,
|
|
186
|
+
ctypes.c_void_p,
|
|
187
|
+
ctypes.c_size_t,
|
|
188
|
+
ctypes.c_void_p,
|
|
189
|
+
ctypes.c_size_t,
|
|
190
|
+
)
|
|
191
|
+
self.generation_result_truncated_fields = _function(
|
|
192
|
+
self.handle,
|
|
193
|
+
"morpheus_generation_result_truncated_fields",
|
|
194
|
+
ctypes.c_uint32,
|
|
195
|
+
ctypes.c_void_p,
|
|
196
|
+
ctypes.c_size_t,
|
|
197
|
+
ctypes.POINTER(ctypes.c_uint32),
|
|
198
|
+
)
|
|
199
|
+
self.generation_result_free = _function(
|
|
200
|
+
self.handle,
|
|
201
|
+
"morpheus_generation_result_free",
|
|
202
|
+
None,
|
|
203
|
+
ctypes.c_void_p,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if self.abi_version() != ABI_VERSION:
|
|
207
|
+
raise RuntimeError("Unsupported libmorpheus ABI version")
|
|
208
|
+
self.analysis_record_size = self.analysis_size()
|
|
209
|
+
if self.analysis_record_size < ctypes.sizeof(AnalysisRecord):
|
|
210
|
+
raise RuntimeError(
|
|
211
|
+
"libmorpheus analysis record is smaller than ABI version 2"
|
|
212
|
+
)
|
|
213
|
+
self.generation_record_size = self.generation_size()
|
|
214
|
+
if self.generation_record_size < ctypes.sizeof(GenerationRecord):
|
|
215
|
+
raise RuntimeError(
|
|
216
|
+
"libmorpheus generation record is smaller than ABI version 2"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def encoded_buffer(value: str, label: str) -> tuple[bytes, object]:
|
|
221
|
+
encoded = value.encode("utf-8")
|
|
222
|
+
if b"\0" in encoded:
|
|
223
|
+
raise ValueError(f"{label} contains a NUL byte")
|
|
224
|
+
buffer = (ctypes.c_uint8 * max(1, len(encoded)))()
|
|
225
|
+
if encoded:
|
|
226
|
+
buffer[: len(encoded)] = encoded
|
|
227
|
+
return encoded, buffer
|
libmorpheus/_library.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Resource-owning Python facade for the libmorpheus ABI."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ctypes
|
|
7
|
+
from os import PathLike, fspath
|
|
8
|
+
from threading import RLock
|
|
9
|
+
from typing import Callable, Self
|
|
10
|
+
|
|
11
|
+
from ._abi import (
|
|
12
|
+
ABI_VERSION,
|
|
13
|
+
GENERATION_OPTIONS_VERSION,
|
|
14
|
+
AnalysisRecord,
|
|
15
|
+
GenerationRecord,
|
|
16
|
+
NativeGenerationOptions,
|
|
17
|
+
NativeLibrary,
|
|
18
|
+
encoded_buffer,
|
|
19
|
+
)
|
|
20
|
+
from ._types import (
|
|
21
|
+
Analysis,
|
|
22
|
+
Generation,
|
|
23
|
+
GenerationOptions,
|
|
24
|
+
Language,
|
|
25
|
+
Option,
|
|
26
|
+
RawAnalysis,
|
|
27
|
+
RawGeneration,
|
|
28
|
+
Status,
|
|
29
|
+
normalize_analysis,
|
|
30
|
+
normalize_generation,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MorpheusError(RuntimeError):
|
|
35
|
+
"""Error reported by the native ABI, including its numeric status."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, status: int, message: str) -> None:
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.status = status
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _message(native: NativeLibrary, status: int) -> str:
|
|
43
|
+
message = native.status_message(status)
|
|
44
|
+
return message.decode("utf-8") if message else f"Morpheus status {status}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _check(native: NativeLibrary, status: int) -> None:
|
|
48
|
+
if status != Status.OK:
|
|
49
|
+
raise MorpheusError(status, _message(native, status))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _text(value: bytes) -> str:
|
|
53
|
+
return value.split(b"\0", 1)[0].decode("utf-8")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _raw_analysis(record: AnalysisRecord, truncated_fields: int) -> RawAnalysis:
|
|
57
|
+
return RawAnalysis(
|
|
58
|
+
struct_size=record.struct_size,
|
|
59
|
+
part_of_speech=record.part_of_speech,
|
|
60
|
+
dialect=record.dialect,
|
|
61
|
+
geographic_region=record.geographic_region,
|
|
62
|
+
person=record.person,
|
|
63
|
+
number=record.number,
|
|
64
|
+
gender=record.gender,
|
|
65
|
+
grammatical_case=record.grammatical_case,
|
|
66
|
+
tense=record.tense,
|
|
67
|
+
mood=record.mood,
|
|
68
|
+
voice=record.voice,
|
|
69
|
+
degree=record.degree,
|
|
70
|
+
raw=_text(record.raw),
|
|
71
|
+
workword=_text(record.workword),
|
|
72
|
+
lemma=_text(record.lemma),
|
|
73
|
+
preverb=_text(record.preverb),
|
|
74
|
+
augment=_text(record.augment),
|
|
75
|
+
stem=_text(record.stem),
|
|
76
|
+
suffix=_text(record.suffix),
|
|
77
|
+
ending=_text(record.ending),
|
|
78
|
+
crasis=_text(record.crasis),
|
|
79
|
+
dictionary_form=_text(record.dictionary_form),
|
|
80
|
+
english_form=_text(record.english_form),
|
|
81
|
+
raw_preverb=_text(record.raw_preverb),
|
|
82
|
+
domains=_text(record.domains),
|
|
83
|
+
morph_flags=bytes(record.morph_flags),
|
|
84
|
+
truncated_fields=truncated_fields,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _raw_generation(
|
|
89
|
+
record: GenerationRecord, truncated_fields: int
|
|
90
|
+
) -> RawGeneration:
|
|
91
|
+
return RawGeneration(
|
|
92
|
+
struct_size=record.struct_size,
|
|
93
|
+
part_of_speech=record.part_of_speech,
|
|
94
|
+
dialect=record.dialect,
|
|
95
|
+
geographic_region=record.geographic_region,
|
|
96
|
+
person=record.person,
|
|
97
|
+
number=record.number,
|
|
98
|
+
gender=record.gender,
|
|
99
|
+
grammatical_case=record.grammatical_case,
|
|
100
|
+
tense=record.tense,
|
|
101
|
+
mood=record.mood,
|
|
102
|
+
voice=record.voice,
|
|
103
|
+
degree=record.degree,
|
|
104
|
+
surface=_text(record.surface),
|
|
105
|
+
lemma=_text(record.lemma),
|
|
106
|
+
morph_flags=bytes(record.morph_flags),
|
|
107
|
+
truncated_fields=truncated_fields,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _generation_options(options: GenerationOptions) -> NativeGenerationOptions:
|
|
112
|
+
if not isinstance(options.result_limit, int) or isinstance(
|
|
113
|
+
options.result_limit, bool
|
|
114
|
+
) or not 0 <= options.result_limit <= 65_536:
|
|
115
|
+
raise ValueError("result_limit must be an integer from 0 to 65536")
|
|
116
|
+
return NativeGenerationOptions(
|
|
117
|
+
version=GENERATION_OPTIONS_VERSION,
|
|
118
|
+
struct_size=ctypes.sizeof(NativeGenerationOptions),
|
|
119
|
+
result_limit=options.result_limit,
|
|
120
|
+
flags=1 if options.exclude_duals else 0,
|
|
121
|
+
part_of_speech=int(options.part_of_speech),
|
|
122
|
+
dialect=int(options.dialect),
|
|
123
|
+
geographic_region=int(options.geographic_region),
|
|
124
|
+
person=int(options.person),
|
|
125
|
+
number=int(options.number),
|
|
126
|
+
gender=int(options.gender),
|
|
127
|
+
grammatical_case=int(options.grammatical_case),
|
|
128
|
+
tense=int(options.tense),
|
|
129
|
+
mood=int(options.mood),
|
|
130
|
+
voice=int(options.voice),
|
|
131
|
+
degree=int(options.degree),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Library:
|
|
136
|
+
"""Owns one validated native library and creates stateful contexts."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, path: str | PathLike[str]) -> None:
|
|
139
|
+
self._native: NativeLibrary | None = NativeLibrary(path)
|
|
140
|
+
self._contexts = 0
|
|
141
|
+
self._lock = RLock()
|
|
142
|
+
|
|
143
|
+
def context(
|
|
144
|
+
self, stemlib_path: str | PathLike[str], language: Language
|
|
145
|
+
) -> Context:
|
|
146
|
+
with self._lock:
|
|
147
|
+
native = self._require_open()
|
|
148
|
+
path = fspath(stemlib_path)
|
|
149
|
+
if not isinstance(path, str):
|
|
150
|
+
raise TypeError("stemlib path must resolve to a string")
|
|
151
|
+
encoded, buffer = encoded_buffer(path, "stemlib path")
|
|
152
|
+
pointer = ctypes.c_void_p()
|
|
153
|
+
status = native.open_path(
|
|
154
|
+
ABI_VERSION,
|
|
155
|
+
buffer,
|
|
156
|
+
len(encoded),
|
|
157
|
+
int(language),
|
|
158
|
+
ctypes.byref(pointer),
|
|
159
|
+
)
|
|
160
|
+
_check(native, status)
|
|
161
|
+
if not pointer:
|
|
162
|
+
raise RuntimeError("libmorpheus returned a null context")
|
|
163
|
+
self._contexts += 1
|
|
164
|
+
return Context(native, pointer, self._context_closed)
|
|
165
|
+
|
|
166
|
+
def close(self) -> None:
|
|
167
|
+
with self._lock:
|
|
168
|
+
if self._native is None:
|
|
169
|
+
return
|
|
170
|
+
if self._contexts:
|
|
171
|
+
raise RuntimeError("Close all Morpheus contexts before the library")
|
|
172
|
+
self._native = None
|
|
173
|
+
|
|
174
|
+
def _require_open(self) -> NativeLibrary:
|
|
175
|
+
if self._native is None:
|
|
176
|
+
raise RuntimeError("Morpheus library is closed")
|
|
177
|
+
return self._native
|
|
178
|
+
|
|
179
|
+
def _context_closed(self) -> None:
|
|
180
|
+
with self._lock:
|
|
181
|
+
self._contexts -= 1
|
|
182
|
+
|
|
183
|
+
def __enter__(self) -> Self:
|
|
184
|
+
self._require_open()
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
def __exit__(self, *_: object) -> None:
|
|
188
|
+
self.close()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class Context:
|
|
192
|
+
"""One language and stemlib context with serialized native operations."""
|
|
193
|
+
|
|
194
|
+
def __init__(
|
|
195
|
+
self,
|
|
196
|
+
native: NativeLibrary,
|
|
197
|
+
pointer: ctypes.c_void_p,
|
|
198
|
+
on_close: Callable[[], None],
|
|
199
|
+
) -> None:
|
|
200
|
+
self._native = native
|
|
201
|
+
self._pointer: ctypes.c_void_p | None = pointer
|
|
202
|
+
self._on_close: Callable[[], None] | None = on_close
|
|
203
|
+
self._lock = RLock()
|
|
204
|
+
|
|
205
|
+
def analyze(
|
|
206
|
+
self, beta_code: str, options: Option = Option.NONE
|
|
207
|
+
) -> tuple[Analysis, ...]:
|
|
208
|
+
return tuple(normalize_analysis(item) for item in self.analyze_raw(beta_code, options))
|
|
209
|
+
|
|
210
|
+
def analyze_raw(
|
|
211
|
+
self, beta_code: str, options: Option = Option.NONE
|
|
212
|
+
) -> tuple[RawAnalysis, ...]:
|
|
213
|
+
encoded, buffer = encoded_buffer(beta_code, "Beta Code input")
|
|
214
|
+
option_value = int(options)
|
|
215
|
+
if option_value < 0 or option_value > (1 << 64) - 1:
|
|
216
|
+
raise ValueError("analysis options exceed uint64")
|
|
217
|
+
with self._lock:
|
|
218
|
+
pointer = self._require_open()
|
|
219
|
+
result = ctypes.c_void_p()
|
|
220
|
+
status = self._native.analyze(
|
|
221
|
+
pointer,
|
|
222
|
+
buffer,
|
|
223
|
+
len(encoded),
|
|
224
|
+
option_value,
|
|
225
|
+
ctypes.byref(result),
|
|
226
|
+
)
|
|
227
|
+
_check(self._native, status)
|
|
228
|
+
try:
|
|
229
|
+
count = self._native.result_count(result)
|
|
230
|
+
analyses = []
|
|
231
|
+
for index in range(count):
|
|
232
|
+
storage = ctypes.create_string_buffer(
|
|
233
|
+
self._native.analysis_record_size
|
|
234
|
+
)
|
|
235
|
+
_check(
|
|
236
|
+
self._native,
|
|
237
|
+
self._native.result_copy(
|
|
238
|
+
result,
|
|
239
|
+
index,
|
|
240
|
+
storage,
|
|
241
|
+
self._native.analysis_record_size,
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
truncated = ctypes.c_uint32()
|
|
245
|
+
_check(
|
|
246
|
+
self._native,
|
|
247
|
+
self._native.result_truncated_fields(
|
|
248
|
+
result, index, ctypes.byref(truncated)
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
record = AnalysisRecord.from_buffer_copy(storage)
|
|
252
|
+
analyses.append(_raw_analysis(record, truncated.value))
|
|
253
|
+
return tuple(analyses)
|
|
254
|
+
finally:
|
|
255
|
+
self._native.result_free(result)
|
|
256
|
+
|
|
257
|
+
def generate(
|
|
258
|
+
self, lemma: str, options: GenerationOptions = GenerationOptions()
|
|
259
|
+
) -> tuple[Generation, ...]:
|
|
260
|
+
"""Generate normalized forms through the experimental native API."""
|
|
261
|
+
return tuple(
|
|
262
|
+
normalize_generation(item) for item in self.generate_raw(lemma, options)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def generate_raw(
|
|
266
|
+
self, lemma: str, options: GenerationOptions = GenerationOptions()
|
|
267
|
+
) -> tuple[RawGeneration, ...]:
|
|
268
|
+
"""Generate lossless ABI records through the experimental native API."""
|
|
269
|
+
encoded, buffer = encoded_buffer(lemma, "Beta Code lemma")
|
|
270
|
+
native_options = _generation_options(options)
|
|
271
|
+
with self._lock:
|
|
272
|
+
pointer = self._require_open()
|
|
273
|
+
result = ctypes.c_void_p()
|
|
274
|
+
status = self._native.generate(
|
|
275
|
+
pointer,
|
|
276
|
+
buffer,
|
|
277
|
+
len(encoded),
|
|
278
|
+
ctypes.byref(native_options),
|
|
279
|
+
ctypes.byref(result),
|
|
280
|
+
)
|
|
281
|
+
_check(self._native, status)
|
|
282
|
+
try:
|
|
283
|
+
count = self._native.generation_result_count(result)
|
|
284
|
+
generations = []
|
|
285
|
+
for index in range(count):
|
|
286
|
+
storage = ctypes.create_string_buffer(
|
|
287
|
+
self._native.generation_record_size
|
|
288
|
+
)
|
|
289
|
+
_check(
|
|
290
|
+
self._native,
|
|
291
|
+
self._native.generation_result_copy(
|
|
292
|
+
result,
|
|
293
|
+
index,
|
|
294
|
+
storage,
|
|
295
|
+
self._native.generation_record_size,
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
truncated = ctypes.c_uint32()
|
|
299
|
+
_check(
|
|
300
|
+
self._native,
|
|
301
|
+
self._native.generation_result_truncated_fields(
|
|
302
|
+
result, index, ctypes.byref(truncated)
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
record = GenerationRecord.from_buffer_copy(storage)
|
|
306
|
+
generations.append(_raw_generation(record, truncated.value))
|
|
307
|
+
return tuple(generations)
|
|
308
|
+
finally:
|
|
309
|
+
self._native.generation_result_free(result)
|
|
310
|
+
|
|
311
|
+
def close(self) -> None:
|
|
312
|
+
with self._lock:
|
|
313
|
+
if self._pointer is None:
|
|
314
|
+
return
|
|
315
|
+
self._native.close_context(self._pointer)
|
|
316
|
+
self._pointer = None
|
|
317
|
+
callback = self._on_close
|
|
318
|
+
self._on_close = None
|
|
319
|
+
if callback is not None:
|
|
320
|
+
callback()
|
|
321
|
+
|
|
322
|
+
def _require_open(self) -> ctypes.c_void_p:
|
|
323
|
+
if self._pointer is None:
|
|
324
|
+
raise RuntimeError("Morpheus context is closed")
|
|
325
|
+
return self._pointer
|
|
326
|
+
|
|
327
|
+
def __enter__(self) -> Self:
|
|
328
|
+
self._require_open()
|
|
329
|
+
return self
|
|
330
|
+
|
|
331
|
+
def __exit__(self, *_: object) -> None:
|
|
332
|
+
self.close()
|