cpp-pinyin2 2.0.0__py3-none-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.
- cpp_pinyin2/__init__.py +860 -0
- cpp_pinyin2/_errors.py +64 -0
- cpp_pinyin2/_ffi.py +411 -0
- cpp_pinyin2/_native/libcpp-pinyin2.dll +0 -0
- cpp_pinyin2/_native/name.txt +1 -0
- cpp_pinyin2/_native.py +80 -0
- cpp_pinyin2/_version.py +4 -0
- cpp_pinyin2/data/dict/cantonese/License.txt +13 -0
- cpp_pinyin2/data/dict/cantonese/dialect.json +15 -0
- cpp_pinyin2/data/dict/cantonese/format.txt +1 -0
- cpp_pinyin2/data/dict/cantonese/phrases_dict.txt +1477 -0
- cpp_pinyin2/data/dict/cantonese/phrases_map.txt +461 -0
- cpp_pinyin2/data/dict/cantonese/trans_word.txt +1799 -0
- cpp_pinyin2/data/dict/cantonese/user_dict.txt +0 -0
- cpp_pinyin2/data/dict/cantonese/word.txt +19483 -0
- cpp_pinyin2/data/dict/mandarin/License.txt +32 -0
- cpp_pinyin2/data/dict/mandarin/dialect.json +15 -0
- cpp_pinyin2/data/dict/mandarin/format.txt +1 -0
- cpp_pinyin2/data/dict/mandarin/phrases_dict.txt +6975 -0
- cpp_pinyin2/data/dict/mandarin/phrases_map.txt +1003 -0
- cpp_pinyin2/data/dict/mandarin/trans_word.txt +6110 -0
- cpp_pinyin2/data/dict/mandarin/user_dict.txt +101 -0
- cpp_pinyin2/data/dict/mandarin/word.txt +17525 -0
- cpp_pinyin2-2.0.0.dist-info/METADATA +509 -0
- cpp_pinyin2-2.0.0.dist-info/RECORD +29 -0
- cpp_pinyin2-2.0.0.dist-info/WHEEL +5 -0
- cpp_pinyin2-2.0.0.dist-info/licenses/LICENSE +202 -0
- cpp_pinyin2-2.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +44 -0
- cpp_pinyin2-2.0.0.dist-info/top_level.txt +1 -0
cpp_pinyin2/_errors.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Exception hierarchy for the cpp-pinyin2 Python shell.
|
|
2
|
+
|
|
3
|
+
Every non-``CPPP2_OK`` status becomes one of these. A shell must never turn a failure into an
|
|
4
|
+
empty result or a partial one (`docs/DECISIONS.md` §2.1).
|
|
5
|
+
The mapping lives in :func:`cpp_pinyin2._ffi.raise_for_status` and ends with a fallback branch,
|
|
6
|
+
so an unknown status raises rather than passing as success.
|
|
7
|
+
|
|
8
|
+
The names are the C ABI's value domain (``CPPP2_OK`` / ``CPPP2_ERR_*``, numerically
|
|
9
|
+
``cpp_pinyin2::v2::Code`` 0..5); the sentence for a code is not written here -- it is
|
|
10
|
+
:func:`cpp_pinyin2.code_text`, which asks the library.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CppPinyinError(Exception):
|
|
17
|
+
"""Base class for every error this package raises."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InvalidArgumentError(CppPinyinError, ValueError):
|
|
21
|
+
"""``CPPP2_ERR_INVALID_ARG`` -- a required argument was NULL or empty.
|
|
22
|
+
|
|
23
|
+
Also what a span's own ``failure_kind`` carries when the library cannot read that span, and
|
|
24
|
+
what ``cppp2_render_syllable`` returns for a structure that does not denote one syllable.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NotReadyError(CppPinyinError):
|
|
29
|
+
"""``CPPP2_ERR_NOT_READY`` -- the handle exists but is not usable.
|
|
30
|
+
|
|
31
|
+
Not reachable through :class:`cpp_pinyin2.Engine` today: ``cppp2_open`` publishes only ready
|
|
32
|
+
handles (an id it cannot resolve is reported as a failure of the call, not as a broken
|
|
33
|
+
handle). Mapped anyway -- a shell that cannot express this status would have to guess what
|
|
34
|
+
the library meant.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DictionaryError(CppPinyinError):
|
|
39
|
+
"""``CPPP2_ERR_LOAD`` -- the path was readable but the table did not load."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DictionaryIncompatibleError(CppPinyinError):
|
|
43
|
+
"""``CPPP2_ERR_DICT_INCOMPATIBLE`` -- the shipped data is NEWER than this library understands.
|
|
44
|
+
|
|
45
|
+
The engine reads the dialect's ``format.txt`` before any table and refuses a value above its
|
|
46
|
+
own ``kSupportedDictFormat`` instead of half-reading the data. The action differs from
|
|
47
|
+
:class:`DictionaryError` (nothing is wrong with the file -- the library is old), which is why
|
|
48
|
+
the C ABI gives it a code of its own and the header says a host can act on it.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InternalError(CppPinyinError):
|
|
53
|
+
"""``CPPP2_ERR_INTERNAL`` -- an exception was caught at the C boundary, or a marshal failed."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UnknownDialectError(NotReadyError):
|
|
57
|
+
"""``CPPP2_ERR_NOT_READY`` from ``cppp2_open``: the id is not in the registry.
|
|
58
|
+
|
|
59
|
+
Reachable since the ABI's entry point reports an unknown id as a status of its own, instead
|
|
60
|
+
of a NULL handle plus a message: the library never resolves an unknown id to another dialect,
|
|
61
|
+
and ``cppp2_open``'s ``detail`` names the id (the shell carries that detail into its message).
|
|
62
|
+
It is a :class:`NotReadyError` because it IS that status -- the refined name is what lets a
|
|
63
|
+
caller catch this case without catching every not-ready failure.
|
|
64
|
+
"""
|
cpp_pinyin2/_ffi.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""ctypes declarations for the cpp-pinyin2 C ABI (``include/cpp-pinyin2/ext/c_api.h``).
|
|
2
|
+
|
|
3
|
+
This file is a transcription of that header and nothing else: no engine behaviour is
|
|
4
|
+
reimplemented here (`docs/DECISIONS.md` §2.1).
|
|
5
|
+
|
|
6
|
+
Every entry point gets explicit ``argtypes``/``restype`` -- not just the ones that return
|
|
7
|
+
pointers. ctypes defaults ``restype`` to ``c_int``, so a pointer-returning function declared
|
|
8
|
+
without it is truncated to 32 bits on a 64-bit host and silently becomes a wild pointer.
|
|
9
|
+
|
|
10
|
+
The surface is the frozen ``cppp_*`` symbols of the header; the list and its count live in
|
|
11
|
+
tests/abi/expected_c_api_symbols.txt, so this module repeats neither.
|
|
12
|
+
``cppp_create`` is deliberately absent: the entry point that took a dialect id with no registry and
|
|
13
|
+
reported failure as "NULL handle plus a message" was replaced by :func:`cppp2_open`, whose status
|
|
14
|
+
code distinguishes an unknown dialect (``CPPP2_ERR_NOT_READY``) from a dictionary that would not
|
|
15
|
+
load (``CPPP2_ERR_LOAD``) -- the two failures the old shape made indistinguishable.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import ctypes
|
|
21
|
+
|
|
22
|
+
from ._errors import (
|
|
23
|
+
CppPinyinError,
|
|
24
|
+
DictionaryError,
|
|
25
|
+
DictionaryIncompatibleError,
|
|
26
|
+
InternalError,
|
|
27
|
+
InvalidArgumentError,
|
|
28
|
+
NotReadyError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# --- status codes (the CPPP2_ERR_* enum in c_api.h) -------------------------
|
|
32
|
+
OK = 0
|
|
33
|
+
INVALID_ARG = 1
|
|
34
|
+
NOT_READY = 2
|
|
35
|
+
LOAD = 3
|
|
36
|
+
INTERNAL = 4
|
|
37
|
+
#: The data is newer than this library understands (the dialect's `format.txt` says a format
|
|
38
|
+
#: generation above `kSupportedDictFormat`). A code of its own because the ACTION differs: nothing
|
|
39
|
+
#: is wrong with the file, the library is old -- upgrade it, or point at older data.
|
|
40
|
+
DICT_INCOMPATIBLE = 5
|
|
41
|
+
|
|
42
|
+
_NAMES = {
|
|
43
|
+
OK: "CPPP2_OK",
|
|
44
|
+
INVALID_ARG: "CPPP2_ERR_INVALID_ARG",
|
|
45
|
+
NOT_READY: "CPPP2_ERR_NOT_READY",
|
|
46
|
+
LOAD: "CPPP2_ERR_LOAD",
|
|
47
|
+
INTERNAL: "CPPP2_ERR_INTERNAL",
|
|
48
|
+
DICT_INCOMPATIBLE: "CPPP2_ERR_DICT_INCOMPATIBLE",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# --- the two value domains of cppp2_syllable (see c_api.h's cppp2_syllable) -
|
|
52
|
+
TONE_KIND_UNSPECIFIED = 0
|
|
53
|
+
TONE_KIND_BASE = 1
|
|
54
|
+
TONE_KIND_SANDHI = 2
|
|
55
|
+
|
|
56
|
+
SOURCE_DICTIONARY = 0
|
|
57
|
+
SOURCE_PHRASE = 1
|
|
58
|
+
SOURCE_USER_DICT = 2
|
|
59
|
+
SOURCE_PLUGIN = 3
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def status_name(status: int) -> str:
|
|
63
|
+
return _NAMES.get(status, f"unknown CPPP_ status {status}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def raise_for_status(status: int, where: str, detail: str = "") -> None:
|
|
67
|
+
"""Turn a C status into an exception. Success is an explicit ``status == OK``.
|
|
68
|
+
|
|
69
|
+
The fallback branch matters: an unrecognised code must not fall through as success
|
|
70
|
+
(that is how "returned nothing" becomes "returned nothing and nobody noticed"), and it
|
|
71
|
+
must not raise ``KeyError`` from a dict lookup either.
|
|
72
|
+
|
|
73
|
+
The same value domain covers ``cppp2_error::code``, so this is also what a ``cppp2_open``
|
|
74
|
+
failure is turned into -- with one refinement in :mod:`cpp_pinyin2`, which maps
|
|
75
|
+
``NOT_READY`` (an id the registry does not know) to
|
|
76
|
+
:class:`~cpp_pinyin2.UnknownDialectError`.
|
|
77
|
+
"""
|
|
78
|
+
if status == OK:
|
|
79
|
+
return
|
|
80
|
+
suffix = f": {detail}" if detail else ""
|
|
81
|
+
message = f"{where} failed with {status_name(status)}{suffix}"
|
|
82
|
+
if status == INVALID_ARG:
|
|
83
|
+
raise InvalidArgumentError(message)
|
|
84
|
+
if status == NOT_READY:
|
|
85
|
+
raise NotReadyError(message)
|
|
86
|
+
if status == LOAD:
|
|
87
|
+
raise DictionaryError(message)
|
|
88
|
+
if status == INTERNAL:
|
|
89
|
+
raise InternalError(message)
|
|
90
|
+
if status == DICT_INCOMPATIBLE:
|
|
91
|
+
raise DictionaryIncompatibleError(message)
|
|
92
|
+
raise CppPinyinError(message)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# --- structures -------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class QueryOpts(ctypes.Structure):
|
|
99
|
+
"""``cppp2_query_opts``: caller-owned, the ONE struct that grows.
|
|
100
|
+
|
|
101
|
+
``struct_size`` must be written *before* ``cppp2_query_opts_init`` is called, because init
|
|
102
|
+
only writes fields inside the size the caller declared.
|
|
103
|
+
|
|
104
|
+
``include_reading`` is APPENDED after every field that existed before it (c_api.h's "HOW
|
|
105
|
+
THIS STRUCT GROWS"): a shell built against the older, shorter declaration keeps reading and
|
|
106
|
+
writing the fields it declared at the same offsets, and simply does not see the new one.
|
|
107
|
+
Inserting a field here instead of appending it would move ``v_to_u``/``mark_neutral_as_5``
|
|
108
|
+
under any pre-existing caller's feet.
|
|
109
|
+
|
|
110
|
+
"APPENDED" MEANS AFTER THE OLD ``sizeof``, not merely after the old last member (S9c), so
|
|
111
|
+
``reserved_0`` -- which claims the four bytes of TRAILING PADDING the first, 32-byte
|
|
112
|
+
declaration of this struct ended in -- comes first: this shell is 40 bytes, ``reserved_0``
|
|
113
|
+
is at 28 and ``include_reading`` at 32. Put at 28 instead, the option lands inside the bytes
|
|
114
|
+
an older caller's ``struct_size = 32`` already covers, and the library reads that caller's
|
|
115
|
+
uninitialised stack word as "build the structure". The member has no meaning to set: the
|
|
116
|
+
library neither reads nor writes it.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
_fields_ = [
|
|
120
|
+
("struct_size", ctypes.c_size_t),
|
|
121
|
+
("style", ctypes.c_int32),
|
|
122
|
+
("on_error", ctypes.c_int32),
|
|
123
|
+
("include_candidates", ctypes.c_int32),
|
|
124
|
+
("v_to_u", ctypes.c_int32),
|
|
125
|
+
("mark_neutral_as_5", ctypes.c_int32),
|
|
126
|
+
# RESERVED -- the claim on the padding described above. Never an option.
|
|
127
|
+
("reserved_0", ctypes.c_int32),
|
|
128
|
+
("include_reading", ctypes.c_int32),
|
|
129
|
+
# RESERVED -- same claim, one declaration later: 36..39 is the trailing padding of a
|
|
130
|
+
# 40-byte struct, and claiming it is what keeps the next option out of bytes an older
|
|
131
|
+
# caller's ``struct_size = 40`` already covers (S9c). Never an option.
|
|
132
|
+
("reserved_1", ctypes.c_int32),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Syllable(ctypes.Structure):
|
|
137
|
+
"""``cppp2_syllable``: a STRUCTURED reading (data, never a rendering).
|
|
138
|
+
|
|
139
|
+
``initial`` is "" for a zero-initial syllable; ``fin`` is the canonical final. The pair is a
|
|
140
|
+
projection of the table reading, so it does not always rebuild the spelling the table stores --
|
|
141
|
+
``spelling`` is that spelling, verbatim, and is what ``cppp2_render_syllable`` renders when a
|
|
142
|
+
caller hands the structure back. No ``struct_size``: the library makes no cross-version
|
|
143
|
+
compatibility promise at all (rule 6 is deleted), so the layout is free to follow the header.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
_fields_ = [
|
|
147
|
+
("initial", ctypes.c_char_p),
|
|
148
|
+
("fin", ctypes.c_char_p),
|
|
149
|
+
("spelling", ctypes.c_char_p),
|
|
150
|
+
("tone", ctypes.c_int32),
|
|
151
|
+
("tone_kind", ctypes.c_int32),
|
|
152
|
+
("source", ctypes.c_int32),
|
|
153
|
+
("reserved_0", ctypes.c_int32), # claims the trailing hole the C compiler used to insert
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Segment(ctypes.Structure):
|
|
158
|
+
"""``cppp2_segment``: one input span and what the engine read for it.
|
|
159
|
+
|
|
160
|
+
The field order is the header's. ``failure_kind`` used to be followed by four bytes of compiler
|
|
161
|
+
padding on a 64-bit host; that hole is now claimed by ``reserved_0`` (S9c), so a future field can
|
|
162
|
+
be appended without changing ``sizeof`` -- and an older consumer's array stride keeps working.
|
|
163
|
+
The order must still stay identical to c_api.h rather than merely "the same fields".
|
|
164
|
+
|
|
165
|
+
``rendered`` is the ONE rendered string of this span (it equals ``text`` when the span
|
|
166
|
+
failed). ``failure_kind`` is a status code -- numerically a ``cpp_pinyin2::v2::Code`` -- and
|
|
167
|
+
``syllables``/``syllable_count`` are the structural side, EMPTY both when the caller did not
|
|
168
|
+
ask for it (``include_reading == 0``) and when the span failed. Read ``failure_kind``, never
|
|
169
|
+
"is the structure filled", to decide whether a span failed.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
_fields_ = [
|
|
173
|
+
("text", ctypes.c_char_p),
|
|
174
|
+
("rendered", ctypes.c_char_p),
|
|
175
|
+
("failure_kind", ctypes.c_int32),
|
|
176
|
+
("reserved_0", ctypes.c_int32), # claims the hole before the pointer, so no padding is implicit
|
|
177
|
+
("syllables", ctypes.POINTER(Syllable)),
|
|
178
|
+
("syllable_count", ctypes.c_size_t),
|
|
179
|
+
("candidate_count", ctypes.c_size_t),
|
|
180
|
+
("candidates", ctypes.POINTER(ctypes.c_char_p)),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Result(ctypes.Structure):
|
|
185
|
+
"""``cppp2_result``. Everything reachable from it dies with ``cppp2_result_free``."""
|
|
186
|
+
|
|
187
|
+
_fields_ = [
|
|
188
|
+
("count", ctypes.c_size_t),
|
|
189
|
+
("segments", ctypes.POINTER(Segment)),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class Error(ctypes.Structure):
|
|
194
|
+
"""``cppp2_error``: the entry points' optional error channel.
|
|
195
|
+
|
|
196
|
+
``detail`` is ``POINTER(c_char)`` rather than ``c_char_p`` on purpose. A ``c_char_p`` field
|
|
197
|
+
reads back as a Python ``bytes`` object, and the address of the library's allocation -- the
|
|
198
|
+
one thing ``cppp2_string_free`` needs -- would be gone: passing those bytes back would hand
|
|
199
|
+
the library a pointer into CPython's own buffer. Owned by the CALLER, NULL on success, and
|
|
200
|
+
released by :func:`take_detail` (or by nothing, if the caller never looks at it -- the
|
|
201
|
+
library writes both fields before returning, so a caller never pre-initialises the struct).
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
_fields_ = [
|
|
205
|
+
("code", ctypes.c_int32),
|
|
206
|
+
("reserved_0", ctypes.c_int32), # claims the hole before `detail`
|
|
207
|
+
("detail", ctypes.POINTER(ctypes.c_char)),
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def take_detail(err: "Error", lib: ctypes.CDLL) -> str:
|
|
212
|
+
"""Decode ``err.detail`` and release it -- the struct's ownership rule, applied once.
|
|
213
|
+
|
|
214
|
+
Safe to call on success (``detail`` is NULL, nothing is freed, "" comes back), so a caller
|
|
215
|
+
does not have to branch before it can clean up.
|
|
216
|
+
"""
|
|
217
|
+
detail = decode(err.detail)
|
|
218
|
+
if err.detail:
|
|
219
|
+
lib.cppp2_string_free(err.detail)
|
|
220
|
+
err.detail = None
|
|
221
|
+
return detail
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# --- declarations -----------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
_STR = ctypes.c_char_p
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Style(ctypes.Structure):
|
|
230
|
+
"""``cppp2_style``: one row of a dialect's own style table -- the same three field names as the
|
|
231
|
+
C++ ``StyleDecl`` (name / alias / isDefault).
|
|
232
|
+
|
|
233
|
+
``is_default`` is 1 for the dialect's own default, else 0: the C ABI carries no ``bool``. The rows
|
|
234
|
+
belong to the library -- release them with ``cppp2_style_list_free``."""
|
|
235
|
+
|
|
236
|
+
_fields_ = [
|
|
237
|
+
("name", _STR),
|
|
238
|
+
("alias", ctypes.c_int32),
|
|
239
|
+
("is_default", ctypes.c_int32),
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
class ToneSandhiOpts(ctypes.Structure):
|
|
243
|
+
"""``cppp2_tone_sandhi_opts``: caller-owned, three switches, and the library's default for all
|
|
244
|
+
three is ON (1).
|
|
245
|
+
|
|
246
|
+
``struct_size`` is written by the CALLER before the call, exactly like ``cppp2_query_opts``: it is
|
|
247
|
+
how the library knows which fields the caller's own header actually declares, so a shell built
|
|
248
|
+
against an older ``c_api.h`` still gets a defined answer instead of a field it never set.
|
|
249
|
+
|
|
250
|
+
This is MANDARIN only -- see ``Engine.tone_sandhi``."""
|
|
251
|
+
|
|
252
|
+
_fields_ = [
|
|
253
|
+
("struct_size", ctypes.c_size_t),
|
|
254
|
+
("third_tone", ctypes.c_int32),
|
|
255
|
+
("bu", ctypes.c_int32),
|
|
256
|
+
("yi", ctypes.c_int32),
|
|
257
|
+
# Claims the trailing padding, as c_api.h:357-359 states: the three switches end at 20 while
|
|
258
|
+
# sizeof is 24, and an unpinned four-byte hole is where a later field would silently land.
|
|
259
|
+
("reserved_0", ctypes.c_int32),
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def declare(lib: ctypes.CDLL) -> None:
|
|
264
|
+
"""Declare every exported entry point of the ABI on ``lib``.
|
|
265
|
+
|
|
266
|
+
"Every" is checked rather than counted here: the authority is
|
|
267
|
+
``tests/abi/expected_c_api_symbols.txt``, and a hard-coded number in this docstring went stale
|
|
268
|
+
once already (it said 20 while the shell declared 23).
|
|
269
|
+
|
|
270
|
+
``cppp2_open``'s first argument is the nullable ``const cppp2_registry *``: the shell has no
|
|
271
|
+
C++ host to hand it one, so it passes NULL and gets the library's own two built-ins -- which
|
|
272
|
+
is exactly what the header says NULL means.
|
|
273
|
+
"""
|
|
274
|
+
lib.cppp2_version.restype = _STR
|
|
275
|
+
lib.cppp2_version.argtypes = []
|
|
276
|
+
|
|
277
|
+
# D2: the native build's installed dictionary root, or NULL when the build has no anchor (static).
|
|
278
|
+
# Not part of `Engine.default_dict_dir()` -- a wheel carries its own copy, and that copy wins there.
|
|
279
|
+
lib.cppp2_dict_dir_candidate.restype = _STR
|
|
280
|
+
lib.cppp2_dict_dir_candidate.argtypes = []
|
|
281
|
+
|
|
282
|
+
lib.cppp2_string_free.restype = None
|
|
283
|
+
# c_void_p, not c_char_p: the free functions are also handed the raw char* of `cppp2_error.detail`
|
|
284
|
+
# and the strlen-free `char**` of `cppp2_string_list_free`. Pointers are the only thing all three
|
|
285
|
+
# call sites have in common, and ctypes converts any pointer instance (including c_char_p) to it.
|
|
286
|
+
lib.cppp2_string_free.argtypes = [ctypes.c_void_p]
|
|
287
|
+
|
|
288
|
+
lib.cppp2_string_list_free.restype = None
|
|
289
|
+
lib.cppp2_string_list_free.argtypes = [ctypes.POINTER(_STR), ctypes.c_size_t]
|
|
290
|
+
lib.cppp2_style_list.restype = ctypes.c_int32
|
|
291
|
+
lib.cppp2_style_list.argtypes = [
|
|
292
|
+
ctypes.c_void_p,
|
|
293
|
+
ctypes.POINTER(ctypes.POINTER(Style)),
|
|
294
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
lib.cppp2_style_list_free.restype = None
|
|
298
|
+
lib.cppp2_style_list_free.argtypes = [ctypes.POINTER(Style), ctypes.c_size_t]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
lib.cppp2_code_text.restype = _STR
|
|
302
|
+
lib.cppp2_code_text.argtypes = [ctypes.c_int32]
|
|
303
|
+
|
|
304
|
+
lib.cppp2_open.restype = ctypes.c_void_p
|
|
305
|
+
lib.cppp2_open.argtypes = [
|
|
306
|
+
ctypes.c_void_p,
|
|
307
|
+
_STR,
|
|
308
|
+
_STR,
|
|
309
|
+
ctypes.POINTER(Error),
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
lib.cppp2_destroy.restype = None
|
|
313
|
+
lib.cppp2_destroy.argtypes = [ctypes.c_void_p]
|
|
314
|
+
|
|
315
|
+
lib.cppp2_dialect_ids.restype = ctypes.c_int32
|
|
316
|
+
lib.cppp2_dialect_ids.argtypes = [
|
|
317
|
+
ctypes.c_void_p,
|
|
318
|
+
ctypes.POINTER(ctypes.POINTER(_STR)),
|
|
319
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
lib.cppp2_query_opts_init.restype = None
|
|
323
|
+
lib.cppp2_query_opts_init.argtypes = [ctypes.POINTER(QueryOpts)]
|
|
324
|
+
|
|
325
|
+
lib.cppp2_query.restype = ctypes.c_int32
|
|
326
|
+
lib.cppp2_query.argtypes = [
|
|
327
|
+
ctypes.c_void_p,
|
|
328
|
+
_STR,
|
|
329
|
+
ctypes.POINTER(QueryOpts),
|
|
330
|
+
ctypes.POINTER(ctypes.POINTER(Result)),
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
lib.cppp2_result_free.restype = None
|
|
334
|
+
lib.cppp2_result_free.argtypes = [ctypes.POINTER(Result)]
|
|
335
|
+
|
|
336
|
+
lib.cppp2_apply_tone_sandhi.restype = ctypes.c_int32
|
|
337
|
+
lib.cppp2_apply_tone_sandhi.argtypes = [
|
|
338
|
+
# The engine handle comes first, and it is NOT decoration: tone sandhi is a dialect property, so the
|
|
339
|
+
# library asks the dialect whether it declares a pass instead of applying the Mandarin rules to any
|
|
340
|
+
# result. A dialect without one answers CPPP2_ERR_INVALID_ARG and leaves the rows alone.
|
|
341
|
+
ctypes.c_void_p,
|
|
342
|
+
ctypes.POINTER(Result),
|
|
343
|
+
ctypes.POINTER(ToneSandhiOpts),
|
|
344
|
+
# May be NULL ("do not report the count"); the return value is a STATUS, like every other
|
|
345
|
+
# entry point here -- the count the C++ function returns travels through this out-parameter.
|
|
346
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
lib.cppp2_render_syllable.restype = ctypes.c_int32
|
|
350
|
+
lib.cppp2_render_syllable.argtypes = [
|
|
351
|
+
ctypes.c_void_p,
|
|
352
|
+
ctypes.POINTER(Syllable),
|
|
353
|
+
ctypes.POINTER(QueryOpts),
|
|
354
|
+
ctypes.POINTER(_STR),
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
lib.cppp2_readings.restype = ctypes.c_int32
|
|
358
|
+
lib.cppp2_readings.argtypes = [
|
|
359
|
+
ctypes.c_void_p,
|
|
360
|
+
_STR,
|
|
361
|
+
ctypes.POINTER(QueryOpts),
|
|
362
|
+
ctypes.POINTER(ctypes.POINTER(_STR)),
|
|
363
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
lib.cppp2_to_simplified.restype = ctypes.c_int32
|
|
367
|
+
lib.cppp2_to_simplified.argtypes = [ctypes.c_void_p, _STR, ctypes.POINTER(_STR)]
|
|
368
|
+
|
|
369
|
+
lib.cppp2_tone_convert.restype = ctypes.c_int32
|
|
370
|
+
lib.cppp2_tone_convert.argtypes = [ctypes.c_void_p, _STR, ctypes.c_int32, ctypes.POINTER(_STR)]
|
|
371
|
+
|
|
372
|
+
lib.cppp2_is_polyphonic.restype = ctypes.c_int32
|
|
373
|
+
lib.cppp2_is_polyphonic.argtypes = [
|
|
374
|
+
ctypes.c_void_p,
|
|
375
|
+
_STR,
|
|
376
|
+
ctypes.POINTER(ctypes.c_int32),
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
lib.cppp2_load_user_dict.restype = ctypes.c_int32
|
|
380
|
+
lib.cppp2_load_user_dict.argtypes = [ctypes.c_void_p, _STR, ctypes.c_int32]
|
|
381
|
+
|
|
382
|
+
lib.cppp2_load_traditional_dict.restype = ctypes.c_int32
|
|
383
|
+
lib.cppp2_load_traditional_dict.argtypes = [ctypes.c_void_p, _STR, ctypes.c_int32]
|
|
384
|
+
|
|
385
|
+
# The `_ex` spellings (2026-10-20): the same two loads plus the error channel. Declared here
|
|
386
|
+
# because python/tests/test_contract.py::test_ffi_declares_exactly_the_c_api_surface compares this module's
|
|
387
|
+
# `lib.cppp2_*` set against the frozen list as SETS -- a symbol the C face has and this module lacks is
|
|
388
|
+
# exactly the gap that test exists to catch. The public Python methods keep calling the short names:
|
|
389
|
+
# their failures already carry `path` from a pre-load check, and switching them is a separate decision.
|
|
390
|
+
lib.cppp2_load_user_dict_ex.restype = ctypes.c_int32
|
|
391
|
+
lib.cppp2_load_user_dict_ex.argtypes = [ctypes.c_void_p, _STR, ctypes.c_int32, ctypes.POINTER(Error)]
|
|
392
|
+
|
|
393
|
+
lib.cppp2_load_traditional_dict_ex.restype = ctypes.c_int32
|
|
394
|
+
lib.cppp2_load_traditional_dict_ex.argtypes = [ctypes.c_void_p, _STR, ctypes.c_int32, ctypes.POINTER(Error)]
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def decode(value) -> str:
|
|
398
|
+
"""Decode a string the library handed out (``bytes``, a pointer) or a NULL (``None``).
|
|
399
|
+
|
|
400
|
+
Three shapes reach here and all three mean "a NUL-terminated UTF-8 string": ``c_char_p``
|
|
401
|
+
fields come back as ``bytes``, ``POINTER(c_char)`` fields (and the ``c_char_p`` instances
|
|
402
|
+
used as out-parameters) come back as pointers, and an out-parameter that failed comes back
|
|
403
|
+
as ``None`` or a NULL pointer.
|
|
404
|
+
"""
|
|
405
|
+
if value is None:
|
|
406
|
+
return ""
|
|
407
|
+
if isinstance(value, bytes):
|
|
408
|
+
return value.decode("utf-8")
|
|
409
|
+
if not value:
|
|
410
|
+
return ""
|
|
411
|
+
return ctypes.string_at(value).decode("utf-8")
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
libcpp-pinyin2.dll
|
cpp_pinyin2/_native.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Locate and load the native library that ships inside this package.
|
|
2
|
+
|
|
3
|
+
The file name is never hardcoded (decision D20=A): MinGW produces ``libcpp-pinyin2.dll``,
|
|
4
|
+
MSVC produces ``cpp-pinyin2.dll``, Linux ``libcpp-pinyin2.so``, macOS ``libcpp-pinyin2.dylib``.
|
|
5
|
+
The build script copies whatever it built into ``cpp_pinyin2/_native/`` and records the actual
|
|
6
|
+
name in ``_native/name.txt``, so the loader re-derives it instead of guessing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ctypes
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
from ._errors import CppPinyinError
|
|
16
|
+
|
|
17
|
+
NAME_FILE = "name.txt"
|
|
18
|
+
_SUFFIXES = (".dll", ".so", ".dylib")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def native_dir() -> Path:
|
|
22
|
+
"""``cpp_pinyin2/_native`` as installed (next to this file)."""
|
|
23
|
+
return Path(__file__).resolve().parent / "_native"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def library_path() -> Path:
|
|
27
|
+
"""The recorded native library, or a diagnosable error -- never a guess."""
|
|
28
|
+
directory = native_dir()
|
|
29
|
+
if not directory.is_dir():
|
|
30
|
+
raise CppPinyinError(
|
|
31
|
+
f"native library directory is missing: {directory}. This package was not "
|
|
32
|
+
f"assembled by python/tools/build_native.py."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
recorded = directory / NAME_FILE
|
|
36
|
+
if recorded.is_file():
|
|
37
|
+
name = recorded.read_text(encoding="utf-8").strip()
|
|
38
|
+
if not name:
|
|
39
|
+
raise CppPinyinError(f"{recorded} is empty: it must record the built library's name.")
|
|
40
|
+
path = directory / name
|
|
41
|
+
if not path.is_file():
|
|
42
|
+
# A recorded name that is not there means a broken package. Scanning for
|
|
43
|
+
# something else would hide exactly the failure the record exists to catch.
|
|
44
|
+
raise CppPinyinError(
|
|
45
|
+
f"{recorded} records {name!r}, but {path} does not exist. "
|
|
46
|
+
f"The package is inconsistent; rebuild it with python/tools/build_native.py."
|
|
47
|
+
)
|
|
48
|
+
return path
|
|
49
|
+
|
|
50
|
+
candidates: List[Path] = sorted(
|
|
51
|
+
f for f in directory.iterdir() if f.is_file() and f.suffix.lower() in _SUFFIXES
|
|
52
|
+
)
|
|
53
|
+
if len(candidates) == 1:
|
|
54
|
+
return candidates[0]
|
|
55
|
+
raise CppPinyinError(
|
|
56
|
+
f"{recorded} is missing and {directory} holds {len(candidates)} library-looking "
|
|
57
|
+
f"files ({', '.join(c.name for c in candidates) or 'none'}), so the name cannot be "
|
|
58
|
+
f"re-derived. Rebuild with python/tools/build_native.py."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_library() -> ctypes.CDLL:
|
|
63
|
+
"""Load the recorded library and declare the ABI on it."""
|
|
64
|
+
from . import _ffi
|
|
65
|
+
|
|
66
|
+
path = library_path()
|
|
67
|
+
try:
|
|
68
|
+
lib = ctypes.CDLL(str(path))
|
|
69
|
+
except OSError as exc:
|
|
70
|
+
# On Windows a missing *dependency* is reported as if the library itself were
|
|
71
|
+
# missing: ctypes does not search PATH for a DLL's dependencies. That is why the
|
|
72
|
+
# build links the MinGW runtime statically (`docs/DECISIONS.md` §2.1
|
|
73
|
+
# section 6.5, item 6); this message is what the user gets if that ever regresses.
|
|
74
|
+
raise CppPinyinError(
|
|
75
|
+
f"could not load {path}: {exc}. If this is Windows, the library may have an "
|
|
76
|
+
f"unresolved dependency (ctypes does not search PATH for dependencies, so a "
|
|
77
|
+
f"non-statically-linked runtime shows up as 'the file is missing')."
|
|
78
|
+
) from exc
|
|
79
|
+
_ffi.declare(lib)
|
|
80
|
+
return lib
|
cpp_pinyin2/_version.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
The Cantonese dictionary files in this folder are based on CC-Canto.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# CC-CEDICT Cantonese Readings
|
|
5
|
+
# Version 2015-09-23
|
|
6
|
+
#
|
|
7
|
+
# Copyright (c) 2015 Pleco Software Incorporated
|
|
8
|
+
# Distributed under a Creative Commons Attribution-Share Alike 3.0 license (http://creativecommons.org/licenses/by-sa/3.0/)
|
|
9
|
+
#
|
|
10
|
+
# For more information + to submit feedback, see our website at cccanto.org
|
|
11
|
+
|
|
12
|
+
CC-Canto and the CC-CEDICT Cantonese readings are copyright (c) 2015-16 Pleco Software Incorporated.
|
|
13
|
+
url: https://github.com/amadeusine/cc-canto-data
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "cantonese",
|
|
3
|
+
"min_library": "2.0.0",
|
|
4
|
+
"required_tables": [
|
|
5
|
+
"word.txt",
|
|
6
|
+
"phrases_dict.txt",
|
|
7
|
+
"phrases_map.txt",
|
|
8
|
+
"trans_word.txt"
|
|
9
|
+
],
|
|
10
|
+
"optional_tables": [
|
|
11
|
+
"user_dict.txt"
|
|
12
|
+
],
|
|
13
|
+
"assets": [
|
|
14
|
+
"License.txt", "format.txt"]
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2
|