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/_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
@@ -0,0 +1,4 @@
1
+ """Generated by python/tools/build_native.py -- do not edit, do not commit."""
2
+
3
+ __version__ = "2.0.0"
4
+ __commit__ = "5d9925b"
@@ -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