icu4py 0.2.0__cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.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.
icu4py/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from icu4py._version import icu_version, icu_version_info
7
+ else:
8
+ from typing import Any
9
+
10
+ def __getattr__(name: str) -> Any:
11
+ if name == "icu_version":
12
+ from icu4py._version import icu_version
13
+
14
+ return icu_version
15
+ elif name == "icu_version_info":
16
+ from icu4py._version import icu_version_info
17
+
18
+ return icu_version_info
19
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
20
+
21
+
22
+ __all__ = ["icu_version", "icu_version_info"]
icu4py/_version.cpp ADDED
@@ -0,0 +1,57 @@
1
+ #define PY_SSIZE_T_CLEAN
2
+ #include <Python.h>
3
+ #include <unicode/uversion.h>
4
+
5
+ namespace {
6
+
7
+ PyMethodDef version_module_methods[] = {
8
+ {nullptr, nullptr, 0, nullptr}
9
+ };
10
+
11
+ int icu4py_version_exec(PyObject* m) {
12
+ UVersionInfo versionArray;
13
+ u_getVersion(versionArray);
14
+
15
+ PyObject* version_tuple = Py_BuildValue("(iiii)",
16
+ versionArray[0],
17
+ versionArray[1],
18
+ versionArray[2],
19
+ versionArray[3]
20
+ );
21
+ if (PyModule_AddObject(m, "icu_version_info", version_tuple) < 0) {
22
+ Py_DECREF(version_tuple);
23
+ return -1;
24
+ }
25
+
26
+ if (PyModule_AddStringConstant(m, "icu_version", U_ICU_VERSION) < 0) {
27
+ return -1;
28
+ }
29
+
30
+ return 0;
31
+ }
32
+
33
+ PyModuleDef_Slot version_slots[] = {
34
+ {Py_mod_exec, reinterpret_cast<void*>(icu4py_version_exec)},
35
+ #ifdef Py_GIL_DISABLED
36
+ {Py_mod_gil, Py_MOD_GIL_NOT_USED},
37
+ #endif
38
+ {0, nullptr}
39
+ };
40
+
41
+ static PyModuleDef versionmodule = {
42
+ PyModuleDef_HEAD_INIT,
43
+ "icu4py._version",
44
+ "ICU version information",
45
+ 0,
46
+ version_module_methods,
47
+ version_slots,
48
+ nullptr,
49
+ nullptr,
50
+ nullptr,
51
+ };
52
+
53
+ } // anonymous namespace
54
+
55
+ PyMODINIT_FUNC PyInit__version() {
56
+ return PyModuleDef_Init(&versionmodule);
57
+ }
icu4py/_version.pyi ADDED
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+
3
+ icu_version: str
4
+ icu_version_info: tuple[int, int, int, int]
icu4py/locale.cpp ADDED
@@ -0,0 +1,267 @@
1
+ #define PY_SSIZE_T_CLEAN
2
+ #include <Python.h>
3
+ #include <string>
4
+ #include <unicode/locid.h>
5
+ #include <unicode/strenum.h>
6
+ #include "locale_types.h"
7
+
8
+ namespace {
9
+
10
+ using icu::Locale;
11
+ using icu4py::LocaleObject;
12
+
13
+ void Locale_dealloc(LocaleObject* self) {
14
+ delete self->locale;
15
+ Py_TYPE(self)->tp_free(reinterpret_cast<PyObject*>(self));
16
+ }
17
+
18
+ PyObject* Locale_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
19
+ auto* self = reinterpret_cast<LocaleObject*>(type->tp_alloc(type, 0));
20
+ if (self != nullptr) {
21
+ self->locale = nullptr;
22
+ }
23
+ return reinterpret_cast<PyObject*>(self);
24
+ }
25
+
26
+ int Locale_init(LocaleObject* self, PyObject* args, PyObject* kwds) {
27
+ const char* language = nullptr;
28
+ const char* country = nullptr;
29
+ const char* variant = nullptr;
30
+ PyObject* extensions = nullptr;
31
+
32
+ static const char* kwlist[] = {"language", "country", "variant", "extensions", nullptr};
33
+
34
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|zzO",
35
+ const_cast<char**>(kwlist),
36
+ &language, &country, &variant, &extensions)) {
37
+ return -1;
38
+ }
39
+
40
+ const char* keywords_and_values = nullptr;
41
+ std::string keywords_str;
42
+
43
+ if (extensions != nullptr && extensions != Py_None) {
44
+ if (!PyDict_Check(extensions)) {
45
+ PyErr_SetString(PyExc_TypeError, "extensions must be a dict or None");
46
+ return -1;
47
+ }
48
+
49
+ Py_ssize_t pos = 0;
50
+ PyObject* key;
51
+ PyObject* value;
52
+ bool first = true;
53
+ bool err = false;
54
+
55
+ #ifdef Py_GIL_DISABLED
56
+ Py_BEGIN_CRITICAL_SECTION(extensions);
57
+ #endif
58
+
59
+ while (PyDict_Next(extensions, &pos, &key, &value)) {
60
+ if (!PyUnicode_Check(key)) {
61
+ PyErr_SetString(PyExc_TypeError, "extension keys must be strings");
62
+ err = true;
63
+ break;
64
+ }
65
+ if (!PyUnicode_Check(value)) {
66
+ PyErr_SetString(PyExc_TypeError, "extension values must be strings");
67
+ err = true;
68
+ break;
69
+ }
70
+
71
+ const char* key_str = PyUnicode_AsUTF8(key);
72
+ if (key_str == nullptr) {
73
+ err = true;
74
+ break;
75
+ }
76
+ const char* value_str = PyUnicode_AsUTF8(value);
77
+ if (value_str == nullptr) {
78
+ err = true;
79
+ break;
80
+ }
81
+
82
+ if (!first) {
83
+ keywords_str += ";";
84
+ }
85
+ keywords_str += key_str;
86
+ keywords_str += "=";
87
+ keywords_str += value_str;
88
+ first = false;
89
+ }
90
+ #ifdef Py_GIL_DISABLED
91
+ Py_END_CRITICAL_SECTION();
92
+ #endif
93
+ if (err) {
94
+ return -1;
95
+ }
96
+
97
+ if (!keywords_str.empty()) {
98
+ keywords_and_values = keywords_str.c_str();
99
+ }
100
+ }
101
+
102
+ self->locale = new Locale(language, country, variant, keywords_and_values);
103
+
104
+ return 0;
105
+ }
106
+
107
+ PyObject* Locale_get_bogus(LocaleObject* self, void* closure) {
108
+ if (self->locale->isBogus()) {
109
+ Py_RETURN_TRUE;
110
+ }
111
+ Py_RETURN_FALSE;
112
+ }
113
+
114
+ PyObject* Locale_get_language(LocaleObject* self, void* closure) {
115
+ const char* language = self->locale->getLanguage();
116
+ return PyUnicode_FromString(language);
117
+ }
118
+
119
+ PyObject* Locale_get_country(LocaleObject* self, void* closure) {
120
+ const char* country = self->locale->getCountry();
121
+ return PyUnicode_FromString(country);
122
+ }
123
+
124
+ PyObject* Locale_get_variant(LocaleObject* self, void* closure) {
125
+ const char* variant = self->locale->getVariant();
126
+ return PyUnicode_FromString(variant);
127
+ }
128
+
129
+ PyObject* Locale_get_extensions(LocaleObject* self, void* closure) {
130
+ UErrorCode status = U_ZERO_ERROR;
131
+ icu::StringEnumeration* keywords = self->locale->createKeywords(status);
132
+
133
+ if (U_FAILURE(status) || keywords == nullptr) {
134
+ return PyDict_New();
135
+ }
136
+
137
+ PyObject* dict = PyDict_New();
138
+ if (dict == nullptr) {
139
+ delete keywords;
140
+ return nullptr;
141
+ }
142
+
143
+ const char* keyword;
144
+ while ((keyword = keywords->next(nullptr, status)) != nullptr) {
145
+ if (U_FAILURE(status)) {
146
+ break;
147
+ }
148
+
149
+ std::string value_str;
150
+ const int32_t capacity = 256;
151
+ char buffer[capacity];
152
+
153
+ int32_t length = self->locale->getKeywordValue(keyword, buffer, capacity, status);
154
+ if (U_FAILURE(status)) {
155
+ break;
156
+ }
157
+
158
+ value_str.assign(buffer, length);
159
+
160
+ PyObject* key_obj = PyUnicode_FromString(keyword);
161
+ PyObject* value_obj = PyUnicode_FromString(value_str.c_str());
162
+
163
+ if (key_obj == nullptr || value_obj == nullptr) {
164
+ Py_XDECREF(key_obj);
165
+ Py_XDECREF(value_obj);
166
+ Py_DECREF(dict);
167
+ delete keywords;
168
+ return nullptr;
169
+ }
170
+
171
+ if (PyDict_SetItem(dict, key_obj, value_obj) < 0) {
172
+ Py_DECREF(key_obj);
173
+ Py_DECREF(value_obj);
174
+ Py_DECREF(dict);
175
+ delete keywords;
176
+ return nullptr;
177
+ }
178
+
179
+ Py_DECREF(key_obj);
180
+ Py_DECREF(value_obj);
181
+ }
182
+
183
+ delete keywords;
184
+
185
+ if (U_FAILURE(status)) {
186
+ Py_DECREF(dict);
187
+ PyErr_SetString(PyExc_RuntimeError, "Failed to retrieve keywords");
188
+ return nullptr;
189
+ }
190
+
191
+ return dict;
192
+ }
193
+
194
+ PyGetSetDef Locale_getsetters[] = {
195
+ {const_cast<char*>("bogus"), reinterpret_cast<getter>(Locale_get_bogus), nullptr,
196
+ const_cast<char*>("Whether the locale is bogus"), nullptr},
197
+ {const_cast<char*>("language"), reinterpret_cast<getter>(Locale_get_language), nullptr,
198
+ const_cast<char*>("The locale's ISO-639 language code"), nullptr},
199
+ {const_cast<char*>("country"), reinterpret_cast<getter>(Locale_get_country), nullptr,
200
+ const_cast<char*>("The locale's ISO-3166 country code"), nullptr},
201
+ {const_cast<char*>("variant"), reinterpret_cast<getter>(Locale_get_variant), nullptr,
202
+ const_cast<char*>("The locale's variant code"), nullptr},
203
+ {const_cast<char*>("extensions"), reinterpret_cast<getter>(Locale_get_extensions), nullptr,
204
+ const_cast<char*>("The locale's keywords and values"), nullptr},
205
+ {nullptr, nullptr, nullptr, nullptr, nullptr}
206
+ };
207
+
208
+ PyType_Slot Locale_slots[] = {
209
+ {Py_tp_doc, const_cast<char*>("ICU Locale")},
210
+ {Py_tp_dealloc, reinterpret_cast<void*>(Locale_dealloc)},
211
+ {Py_tp_init, reinterpret_cast<void*>(Locale_init)},
212
+ {Py_tp_new, reinterpret_cast<void*>(Locale_new)},
213
+ {Py_tp_getset, Locale_getsetters},
214
+ {0, nullptr}
215
+ };
216
+
217
+ PyType_Spec Locale_spec = {
218
+ "icu4py.locale.Locale",
219
+ sizeof(LocaleObject),
220
+ 0,
221
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
222
+ Locale_slots
223
+ };
224
+
225
+ PyMethodDef locale_module_methods[] = {
226
+ {nullptr, nullptr, 0, nullptr}
227
+ };
228
+
229
+ int icu4py_locale_exec(PyObject* m) {
230
+ PyObject* type_obj = PyType_FromModuleAndSpec(m, &Locale_spec, nullptr);
231
+ if (type_obj == nullptr) {
232
+ return -1;
233
+ }
234
+
235
+ if (PyModule_AddObject(m, "Locale", type_obj) < 0) {
236
+ Py_DECREF(type_obj);
237
+ return -1;
238
+ }
239
+
240
+ return 0;
241
+ }
242
+
243
+ PyModuleDef_Slot locale_slots[] = {
244
+ {Py_mod_exec, reinterpret_cast<void*>(icu4py_locale_exec)},
245
+ #ifdef Py_GIL_DISABLED
246
+ {Py_mod_gil, Py_MOD_GIL_NOT_USED},
247
+ #endif
248
+ {0, nullptr}
249
+ };
250
+
251
+ static PyModuleDef localemodule = {
252
+ PyModuleDef_HEAD_INIT,
253
+ "icu4py.locale",
254
+ "",
255
+ 0,
256
+ locale_module_methods,
257
+ locale_slots,
258
+ nullptr,
259
+ nullptr,
260
+ nullptr,
261
+ };
262
+
263
+ } // anonymous namespace
264
+
265
+ PyMODINIT_FUNC PyInit_locale() {
266
+ return PyModuleDef_Init(&localemodule);
267
+ }
icu4py/locale.pyi ADDED
@@ -0,0 +1,26 @@
1
+ from typing import overload
2
+
3
+ from typing_extensions import disjoint_base
4
+
5
+ @disjoint_base
6
+ class Locale:
7
+ @overload
8
+ def __init__(self, language: str) -> None: ...
9
+ @overload
10
+ def __init__(
11
+ self,
12
+ language: str,
13
+ country: str,
14
+ variant: str | None = None,
15
+ extensions: dict[str, str] | None = None,
16
+ ) -> None: ...
17
+ @property
18
+ def bogus(self) -> bool: ...
19
+ @property
20
+ def language(self) -> str: ...
21
+ @property
22
+ def country(self) -> str: ...
23
+ @property
24
+ def variant(self) -> str: ...
25
+ @property
26
+ def extensions(self) -> dict[str, str]: ...
@@ -0,0 +1,522 @@
1
+ #define PY_SSIZE_T_CLEAN
2
+ #include <Python.h>
3
+ #include <unicode/msgfmt.h>
4
+ #include <unicode/unistr.h>
5
+ #include <unicode/fmtable.h>
6
+ #include <unicode/locid.h>
7
+ #include <unicode/parsepos.h>
8
+ #include <unicode/ustring.h>
9
+ #include <unicode/utypes.h>
10
+
11
+ #include <cstring>
12
+ #include <memory>
13
+
14
+ #include "locale_types.h"
15
+
16
+ namespace {
17
+
18
+ using icu::MessageFormat;
19
+ using icu::UnicodeString;
20
+ using icu::Formattable;
21
+ using icu::Locale;
22
+ using icu::FieldPosition;
23
+ using icu::StringPiece;
24
+ using icu4py::LocaleObject;
25
+
26
+ struct ModuleState {
27
+ PyObject* datetime_datetime_type;
28
+ PyObject* datetime_date_type;
29
+ PyObject* datetime_time_type;
30
+ PyObject* decimal_decimal_type;
31
+ PyObject* locale_type;
32
+ };
33
+
34
+ static inline ModuleState* get_module_state(PyObject* module) {
35
+ void* state = PyModule_GetState(module);
36
+ return static_cast<ModuleState*>(state);
37
+ }
38
+
39
+ int icu4py_messageformat_exec(PyObject* m);
40
+ int icu4py_messageformat_traverse(PyObject* m, visitproc visit, void* arg);
41
+ int icu4py_messageformat_clear(PyObject* m);
42
+
43
+ PyMethodDef icu4py_messageformat_module_methods[] = {
44
+ {nullptr, nullptr, 0, nullptr}
45
+ };
46
+
47
+ PyModuleDef_Slot icu4py_messageformat_slots[] = {
48
+ {Py_mod_exec, reinterpret_cast<void*>(icu4py_messageformat_exec)},
49
+ // On Python 3.13+, declare free-threaded support.
50
+ // https://py-free-threading.github.io/porting-extensions/#declaring-free-threaded-support
51
+ #ifdef Py_GIL_DISABLED
52
+ {Py_mod_gil, Py_MOD_GIL_NOT_USED},
53
+ #endif
54
+ {0, nullptr}
55
+ };
56
+
57
+ static PyModuleDef icu4pymodule = {
58
+ PyModuleDef_HEAD_INIT,
59
+ "icu4py.messageformat",
60
+ "",
61
+ sizeof(ModuleState),
62
+ icu4py_messageformat_module_methods,
63
+ icu4py_messageformat_slots,
64
+ icu4py_messageformat_traverse,
65
+ icu4py_messageformat_clear,
66
+ nullptr,
67
+ };
68
+
69
+ struct MessageFormatObject {
70
+ PyObject_HEAD
71
+ MessageFormat* formatter;
72
+ };
73
+
74
+ void MessageFormat_dealloc(MessageFormatObject* self) {
75
+ delete self->formatter;
76
+ Py_TYPE(self)->tp_free(reinterpret_cast<PyObject*>(self));
77
+ }
78
+
79
+ PyObject* MessageFormat_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
80
+ auto* self = reinterpret_cast<MessageFormatObject*>(type->tp_alloc(type, 0));
81
+ if (self != nullptr) {
82
+ self->formatter = nullptr;
83
+ }
84
+ return reinterpret_cast<PyObject*>(self);
85
+ }
86
+
87
+ int MessageFormat_init(MessageFormatObject* self, PyObject* args, PyObject* kwds) {
88
+ const char* pattern;
89
+ PyObject* locale_obj;
90
+ Py_ssize_t pattern_len;
91
+
92
+ static const char* kwlist[] = {"pattern", "locale", nullptr};
93
+
94
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "s#O",
95
+ const_cast<char**>(kwlist),
96
+ &pattern, &pattern_len, &locale_obj)) {
97
+ return -1;
98
+ }
99
+
100
+ #if PY_VERSION_HEX < 0x030B0000
101
+ PyObject* module = _PyType_GetModuleByDef(Py_TYPE(self), &icu4pymodule);
102
+ #else
103
+ PyObject* module = PyType_GetModuleByDef(Py_TYPE(self), &icu4pymodule);
104
+ #endif
105
+ if (module == nullptr) {
106
+ return -1;
107
+ }
108
+ ModuleState* mod_state = get_module_state(module);
109
+
110
+ UErrorCode status = U_ZERO_ERROR;
111
+ UnicodeString upattern = UnicodeString::fromUTF8(StringPiece(pattern, pattern_len));
112
+ Locale locale;
113
+
114
+ if (PyUnicode_Check(locale_obj)) {
115
+ const char* locale_str = PyUnicode_AsUTF8(locale_obj);
116
+ if (locale_str == nullptr) {
117
+ return -1;
118
+ }
119
+ locale = Locale(locale_str);
120
+ } else {
121
+ int is_locale = PyObject_IsInstance(locale_obj, mod_state->locale_type);
122
+ if (is_locale == -1) {
123
+ return -1;
124
+ }
125
+ if (is_locale == 0) {
126
+ PyErr_SetString(PyExc_TypeError, "locale must be a string or Locale object");
127
+ return -1;
128
+ }
129
+
130
+ LocaleObject* locale_pyobj = reinterpret_cast<LocaleObject*>(locale_obj);
131
+ if (locale_pyobj->locale == nullptr) {
132
+ PyErr_SetString(PyExc_ValueError, "Locale object has null internal locale");
133
+ return -1;
134
+ }
135
+ locale = *locale_pyobj->locale;
136
+ }
137
+
138
+ self->formatter = new MessageFormat(upattern, locale, status);
139
+
140
+ if (U_FAILURE(status)) {
141
+ delete self->formatter;
142
+ self->formatter = nullptr;
143
+ PyErr_Format(PyExc_ValueError, "Failed to create MessageFormat: %s",
144
+ u_errorName(status));
145
+ return -1;
146
+ }
147
+
148
+ return 0;
149
+ }
150
+
151
+ bool pyobject_to_formattable(PyObject* obj, Formattable& formattable, ModuleState* state) {
152
+ if (PyLong_Check(obj)) {
153
+ int overflow;
154
+ long long long_val = PyLong_AsLongLongAndOverflow(obj, &overflow);
155
+ if (overflow != 0) {
156
+ PyObject* str_obj = PyObject_Str(obj);
157
+ if (str_obj == nullptr) {
158
+ return false;
159
+ }
160
+ Py_ssize_t size;
161
+ const char* str_val = PyUnicode_AsUTF8AndSize(str_obj, &size);
162
+ if (str_val == nullptr) {
163
+ Py_DECREF(str_obj);
164
+ return false;
165
+ }
166
+ UErrorCode status = U_ZERO_ERROR;
167
+ formattable = Formattable(StringPiece(str_val, size), status);
168
+ Py_DECREF(str_obj);
169
+ if (U_FAILURE(status)) {
170
+ PyErr_Format(PyExc_ValueError, "Failed to create Formattable from overflowed int: %s",
171
+ u_errorName(status));
172
+ return false;
173
+ }
174
+ return true;
175
+ }
176
+ if (long_val == -1 && PyErr_Occurred()) {
177
+ return false;
178
+ }
179
+ formattable = Formattable(static_cast<int64_t>(long_val));
180
+ return true;
181
+ }
182
+
183
+ if (PyUnicode_Check(obj)) {
184
+ Py_ssize_t size;
185
+ const char* str_val = PyUnicode_AsUTF8AndSize(obj, &size);
186
+ if (str_val == nullptr) {
187
+ return false;
188
+ }
189
+ formattable = Formattable(UnicodeString::fromUTF8(StringPiece(str_val, size)));
190
+ return true;
191
+ }
192
+
193
+ if (PyFloat_Check(obj)) {
194
+ double dbl_val = PyFloat_AsDouble(obj);
195
+ if (dbl_val == -1.0 && PyErr_Occurred()) {
196
+ return false;
197
+ }
198
+ formattable = Formattable(dbl_val);
199
+ return true;
200
+ }
201
+
202
+ int is_decimal = PyObject_IsInstance(obj, state->decimal_decimal_type);
203
+ if (is_decimal == -1) {
204
+ return false;
205
+ } else if (is_decimal == 1) {
206
+ PyObject* str_obj = PyObject_Str(obj);
207
+ if (str_obj == nullptr) {
208
+ return false;
209
+ }
210
+ Py_ssize_t size;
211
+ const char* str_val = PyUnicode_AsUTF8AndSize(str_obj, &size);
212
+ if (str_val == nullptr) {
213
+ Py_DECREF(str_obj);
214
+ return false;
215
+ }
216
+ UErrorCode status = U_ZERO_ERROR;
217
+ formattable = Formattable(StringPiece(str_val, size), status);
218
+ Py_DECREF(str_obj);
219
+ if (U_FAILURE(status)) {
220
+ PyErr_Format(PyExc_ValueError, "Failed to create Formattable from Decimal: %s",
221
+ u_errorName(status));
222
+ return false;
223
+ }
224
+ return true;
225
+ }
226
+
227
+ int is_datetime = PyObject_IsInstance(obj, state->datetime_datetime_type);
228
+ if (is_datetime == -1) {
229
+ return false;
230
+ } else if (is_datetime == 1) {
231
+ PyObject* timestamp = PyObject_CallMethod(obj, "timestamp", nullptr);
232
+ if (timestamp == nullptr) {
233
+ return false;
234
+ }
235
+ double timestamp_seconds = PyFloat_AsDouble(timestamp);
236
+ Py_DECREF(timestamp);
237
+ if (timestamp_seconds == -1.0 && PyErr_Occurred()) {
238
+ return false;
239
+ }
240
+ UDate udate = timestamp_seconds * 1000.0;
241
+ formattable = Formattable(udate, Formattable::kIsDate);
242
+ return true;
243
+ }
244
+
245
+ int is_date = PyObject_IsInstance(obj, state->datetime_date_type);
246
+ if (is_date == -1) {
247
+ return false;
248
+ } else if (is_date == 1) {
249
+ PyObject* combine = PyObject_GetAttrString(state->datetime_datetime_type, "combine");
250
+ if (combine == nullptr) {
251
+ return false;
252
+ }
253
+
254
+ PyObject* min_time = PyObject_GetAttrString(state->datetime_time_type, "min");
255
+ if (min_time == nullptr) {
256
+ Py_DECREF(combine);
257
+ return false;
258
+ }
259
+
260
+ PyObject* dt = PyObject_CallFunctionObjArgs(combine, obj, min_time, nullptr);
261
+ Py_DECREF(combine);
262
+ Py_DECREF(min_time);
263
+ if (dt == nullptr) {
264
+ return false;
265
+ }
266
+
267
+ PyObject* timestamp = PyObject_CallMethod(dt, "timestamp", nullptr);
268
+ Py_DECREF(dt);
269
+ if (timestamp == nullptr) {
270
+ return false;
271
+ }
272
+ double timestamp_seconds = PyFloat_AsDouble(timestamp);
273
+ Py_DECREF(timestamp);
274
+ if (timestamp_seconds == -1.0 && PyErr_Occurred()) {
275
+ return false;
276
+ }
277
+ UDate udate = timestamp_seconds * 1000.0;
278
+ formattable = Formattable(udate, Formattable::kIsDate);
279
+ return true;
280
+ }
281
+
282
+ PyErr_SetString(PyExc_TypeError, "Parameter values must be int, float, str, Decimal, datetime, or date");
283
+ return false;
284
+ }
285
+
286
+ bool dict_to_parallel_arrays(PyObject* dict, ModuleState* mod_state, UnicodeString*& names,
287
+ Formattable*& values, int32_t& count) {
288
+ if (!PyDict_Check(dict)) {
289
+ PyErr_SetString(PyExc_TypeError, "Argument must be a dictionary");
290
+ return false;
291
+ }
292
+
293
+ count = static_cast<int32_t>(PyDict_Size(dict));
294
+ if (count == 0) {
295
+ names = nullptr;
296
+ values = nullptr;
297
+ return true;
298
+ }
299
+
300
+ auto names_ptr = std::make_unique<UnicodeString[]>(count);
301
+ auto values_ptr = std::make_unique<Formattable[]>(count);
302
+
303
+ Py_ssize_t pos = 0;
304
+ PyObject* key;
305
+ PyObject* value;
306
+ int32_t i = 0;
307
+ bool err = false;
308
+
309
+ #ifdef Py_GIL_DISABLED
310
+ Py_BEGIN_CRITICAL_SECTION(dict);
311
+ #endif
312
+ while (PyDict_Next(dict, &pos, &key, &value)) {
313
+ // Ensure we don't exceed allocated space
314
+ if (i >= count) {
315
+ PyErr_SetString(PyExc_RuntimeError, "Dictionary size changed during iteration");
316
+ err = true;
317
+ break;
318
+ }
319
+
320
+ if (key == nullptr) {
321
+ PyErr_SetString(PyExc_TypeError, "NULL key in dictionary");
322
+ err = true;
323
+ break;
324
+ }
325
+ if (value == nullptr) {
326
+ PyErr_SetString(PyExc_TypeError, "NULL value in dictionary");
327
+ err = true;
328
+ break;
329
+ }
330
+
331
+ Py_ssize_t key_size;
332
+ const char* key_str = PyUnicode_AsUTF8AndSize(key, &key_size);
333
+ if (key_str == nullptr) {
334
+ PyErr_SetString(PyExc_TypeError, "Dictionary keys must be strings");
335
+ err = true;
336
+ break;
337
+ }
338
+ names_ptr[i] = UnicodeString::fromUTF8(StringPiece(key_str, key_size));
339
+
340
+ if (!pyobject_to_formattable(value, values_ptr[i], mod_state)) {
341
+ if (!PyErr_Occurred()) {
342
+ PyErr_SetString(PyExc_TypeError, "Failed to convert dictionary value to Formattable");
343
+ }
344
+ err = true;
345
+ break;
346
+ }
347
+ ++i;
348
+ }
349
+ #ifdef Py_GIL_DISABLED
350
+ Py_END_CRITICAL_SECTION();
351
+ #endif
352
+ if (err) {
353
+ return false;
354
+ }
355
+
356
+ names = names_ptr.release();
357
+ values = values_ptr.release();
358
+ return true;
359
+ }
360
+
361
+ PyObject* MessageFormat_format(MessageFormatObject* self, PyObject* args) {
362
+ PyObject* params_dict;
363
+
364
+ if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &params_dict)) {
365
+ return nullptr;
366
+ }
367
+
368
+ #if PY_VERSION_HEX < 0x030B0000
369
+ PyObject* module = _PyType_GetModuleByDef(Py_TYPE(self), &icu4pymodule);
370
+ #else
371
+ PyObject* module = PyType_GetModuleByDef(Py_TYPE(self), &icu4pymodule);
372
+ #endif
373
+ if (module == nullptr) {
374
+ return nullptr;
375
+ }
376
+ ModuleState* mod_state = get_module_state(module);
377
+
378
+ UnicodeString* argumentNames = nullptr;
379
+ Formattable* arguments = nullptr;
380
+ int32_t count = 0;
381
+
382
+ if (!dict_to_parallel_arrays(params_dict, mod_state, argumentNames, arguments, count)) {
383
+ return nullptr;
384
+ }
385
+
386
+ auto names_guard = std::unique_ptr<UnicodeString[]>(argumentNames);
387
+ auto values_guard = std::unique_ptr<Formattable[]>(arguments);
388
+
389
+ UErrorCode status = U_ZERO_ERROR;
390
+ UnicodeString result;
391
+
392
+ // ICU objects need external synchronization
393
+ #ifdef Py_GIL_DISABLED
394
+ Py_BEGIN_CRITICAL_SECTION(self);
395
+ #endif
396
+
397
+ if (count == 0) {
398
+ FieldPosition field_pos;
399
+ result = self->formatter->format(nullptr, 0, result, field_pos, status);
400
+ } else {
401
+ result = self->formatter->format(argumentNames, arguments, count, result, status);
402
+ }
403
+
404
+ #ifdef Py_GIL_DISABLED
405
+ Py_END_CRITICAL_SECTION();
406
+ #endif
407
+
408
+ if (U_FAILURE(status)) {
409
+ PyErr_Format(PyExc_RuntimeError, "Failed to format message: %s",
410
+ u_errorName(status));
411
+ return nullptr;
412
+ }
413
+
414
+ std::string utf8;
415
+ result.toUTF8String(utf8);
416
+ return PyUnicode_FromStringAndSize(utf8.c_str(), utf8.size());
417
+ }
418
+
419
+ PyMethodDef MessageFormat_methods[] = {
420
+ {"format", reinterpret_cast<PyCFunction>(MessageFormat_format), METH_VARARGS,
421
+ "Format the message with given parameters"},
422
+ {nullptr, nullptr, 0, nullptr}
423
+ };
424
+
425
+ PyType_Slot MessageFormat_slots[] = {
426
+ {Py_tp_doc, const_cast<char*>("ICU MessageFormat")},
427
+ {Py_tp_dealloc, reinterpret_cast<void*>(MessageFormat_dealloc)},
428
+ {Py_tp_init, reinterpret_cast<void*>(MessageFormat_init)},
429
+ {Py_tp_new, reinterpret_cast<void*>(MessageFormat_new)},
430
+ {Py_tp_methods, MessageFormat_methods},
431
+ {0, nullptr}
432
+ };
433
+
434
+ PyType_Spec MessageFormat_spec = {
435
+ "icu4py.messageformat.MessageFormat",
436
+ sizeof(MessageFormatObject),
437
+ 0,
438
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
439
+ MessageFormat_slots
440
+ };
441
+
442
+ int icu4py_messageformat_exec(PyObject* m) {
443
+ PyObject* type_obj = PyType_FromModuleAndSpec(m, &MessageFormat_spec, nullptr);
444
+ if (type_obj == nullptr) {
445
+ return -1;
446
+ }
447
+
448
+ if (PyModule_AddObject(m, "MessageFormat", type_obj) < 0) {
449
+ Py_DECREF(type_obj);
450
+ return -1;
451
+ }
452
+
453
+ ModuleState* state = get_module_state(m);
454
+
455
+ PyObject* datetime_module = PyImport_ImportModule("datetime");
456
+ if (datetime_module == nullptr) {
457
+ return -1;
458
+ }
459
+
460
+ state->datetime_datetime_type = PyObject_GetAttrString(datetime_module, "datetime");
461
+ state->datetime_date_type = PyObject_GetAttrString(datetime_module, "date");
462
+ state->datetime_time_type = PyObject_GetAttrString(datetime_module, "time");
463
+ Py_DECREF(datetime_module);
464
+
465
+ if (state->datetime_datetime_type == nullptr ||
466
+ state->datetime_date_type == nullptr ||
467
+ state->datetime_time_type == nullptr) {
468
+ return -1;
469
+ }
470
+
471
+ PyObject* decimal_module = PyImport_ImportModule("decimal");
472
+ if (decimal_module == nullptr) {
473
+ return -1;
474
+ }
475
+
476
+ state->decimal_decimal_type = PyObject_GetAttrString(decimal_module, "Decimal");
477
+ Py_DECREF(decimal_module);
478
+
479
+ if (state->decimal_decimal_type == nullptr) {
480
+ return -1;
481
+ }
482
+
483
+ PyObject* locale_module = PyImport_ImportModule("icu4py.locale");
484
+ if (locale_module == nullptr) {
485
+ return -1;
486
+ }
487
+
488
+ state->locale_type = PyObject_GetAttrString(locale_module, "Locale");
489
+ Py_DECREF(locale_module);
490
+
491
+ if (state->locale_type == nullptr) {
492
+ return -1;
493
+ }
494
+
495
+ return 0;
496
+ }
497
+
498
+ int icu4py_messageformat_traverse(PyObject* m, visitproc visit, void* arg) {
499
+ ModuleState* state = get_module_state(m);
500
+ Py_VISIT(state->datetime_datetime_type);
501
+ Py_VISIT(state->datetime_date_type);
502
+ Py_VISIT(state->datetime_time_type);
503
+ Py_VISIT(state->decimal_decimal_type);
504
+ Py_VISIT(state->locale_type);
505
+ return 0;
506
+ }
507
+
508
+ int icu4py_messageformat_clear(PyObject* m) {
509
+ ModuleState* state = get_module_state(m);
510
+ Py_CLEAR(state->datetime_datetime_type);
511
+ Py_CLEAR(state->datetime_date_type);
512
+ Py_CLEAR(state->datetime_time_type);
513
+ Py_CLEAR(state->decimal_decimal_type);
514
+ Py_CLEAR(state->locale_type);
515
+ return 0;
516
+ }
517
+
518
+ } // anonymous namespace
519
+
520
+ PyMODINIT_FUNC PyInit_messageformat() {
521
+ return PyModuleDef_Init(&icu4pymodule);
522
+ }
@@ -0,0 +1,13 @@
1
+ from datetime import date, datetime
2
+ from decimal import Decimal
3
+
4
+ from typing_extensions import disjoint_base
5
+
6
+ from icu4py.locale import Locale
7
+
8
+ @disjoint_base
9
+ class MessageFormat:
10
+ def __init__(self, pattern: str, locale: str | Locale) -> None: ...
11
+ def format(
12
+ self, params: dict[str, int | float | str | Decimal | date | datetime]
13
+ ) -> str: ...
icu4py/py.typed ADDED
File without changes
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: icu4py
3
+ Version: 0.2.0
4
+ Summary: Bindings to the ICU (International Components for Unicode) library (ICU4C).
5
+ Author-email: Adam Johnson <me@adamj.eu>
6
+ License-Expression: MIT
7
+ Project-URL: Changelog, https://icu4py.readthedocs.io/en/latest/changelog.html
8
+ Project-URL: Documentation, https://icu4py.readthedocs.io/
9
+ Project-URL: Funding, https://adamj.eu/books/
10
+ Project-URL: Repository, https://github.com/adamchainz/icu4py
11
+ Keywords: i18n,icu,icu4c,internationalization,l10n,localization,unicode
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Framework :: Pytest
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/x-rst
26
+ License-File: LICENSE
27
+ Requires-Dist: typing-extensions>=4.15
28
+ Dynamic: license-file
29
+
30
+ ======
31
+ icu4py
32
+ ======
33
+
34
+ .. image:: https://img.shields.io/readthedocs/icu4py?style=for-the-badge
35
+ :target: https://icu4py.readthedocs.io/en/latest/
36
+
37
+ .. image:: https://img.shields.io/github/actions/workflow/status/adamchainz/icu4py/main.yml.svg?branch=main&style=for-the-badge
38
+ :target: https://github.com/adamchainz/icu4py/actions?workflow=CI
39
+
40
+ .. image:: https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge
41
+ :target: https://github.com/adamchainz/icu4py/actions?workflow=CI
42
+
43
+ .. image:: https://img.shields.io/pypi/v/icu4py.svg?style=for-the-badge
44
+ :target: https://pypi.org/project/icu4py/
45
+
46
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge
47
+ :target: https://github.com/psf/black
48
+
49
+ .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge
50
+ :target: https://github.com/pre-commit/pre-commit
51
+ :alt: pre-commit
52
+
53
+ ----
54
+
55
+ Bindings to the ICU (International Components for Unicode) library (ICU4C).
56
+
57
+ Documentation
58
+ =============
59
+
60
+ Please see https://icu4py.readthedocs.io/.
@@ -0,0 +1,20 @@
1
+ icu4py/__init__.py,sha256=yd6ls9f_x8oeU3r7IckxKbisUmRm248CXOhAIYiUN1o,603
2
+ icu4py/_version.cpp,sha256=idstiuGpfe-qXcv0tiAWOAUpSmDKsPzqB3sQDVu-cTc,1198
3
+ icu4py/_version.cpython-314-i386-linux-gnu.so,sha256=11vmxJgF0-4dFXoyH3NwjB-DCuhQmOdkCQl18K8EZho,49905
4
+ icu4py/_version.pyi,sha256=4Y8qCvpvU9n5aYm4a91lMh4KKoxe1zl-i-RD6mnAYiY,97
5
+ icu4py/locale.cpp,sha256=Utkys6iRn2cyzySGEwtA3naXou59Bw5Yp5lpIr3rdYs,7522
6
+ icu4py/locale.cpython-314-i386-linux-gnu.so,sha256=fOVUMia4XPRejprAzRpza-Y3fAKXq7Kj24sq3ipcjFc,120249
7
+ icu4py/locale.pyi,sha256=em0XwMnghSxJzvYBbP-5Z9eei9SORkCMIJ95FciyyL8,618
8
+ icu4py/messageformat.cpp,sha256=J8VDS81sDUdRooNao6pQjc2WPx91XuqO358QoVhwlzQ,16065
9
+ icu4py/messageformat.cpython-314-i386-linux-gnu.so,sha256=meunm5SDHKHSUd99OTxglLVC54Ocp9e7FBEog6DwRNU,197377
10
+ icu4py/messageformat.pyi,sha256=CXBlWn45WKsj9mJuPyCNifTKzqF56_taMgqpDhXrEkk,365
11
+ icu4py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ icu4py.libs/libicudata-6b8dd726.so.60.3,sha256=eE8crGu5rj1j2ahANkP8GExDiXXIls_j4Hw-tjB0kKM,26911137
13
+ icu4py.libs/libicui18n-b2c40b87.so.60.3,sha256=xR30TLPf4pDZakai9iHJELFul28kCIXZ_48kv8cqqIY,4836529
14
+ icu4py.libs/libicuuc-0090835d.so.60.3,sha256=3ktbP4bPVukKK0ZKvm5Qmpnuo0nnzuROfDDBzbtXP20,2775321
15
+ icu4py-0.2.0.dist-info/METADATA,sha256=1Jc-hWpxxXk-8pAcWnC3irtwxHD-M_kDBEKByNFp5gE,2325
16
+ icu4py-0.2.0.dist-info/WHEEL,sha256=CL7kcfXt3Qi0BlfapvDaMM5T4wH7ClmYaslWbc5vTcE,148
17
+ icu4py-0.2.0.dist-info/top_level.txt,sha256=bT8eoOy2eVL3nNv2IXqQo-KpPtbBoR415Kow3HGtId8,7
18
+ icu4py-0.2.0.dist-info/RECORD,,
19
+ icu4py-0.2.0.dist-info/licenses/LICENSE,sha256=blVuFA7Qdp2_CcTyGXLxA4CsHsAjAquxVc4O6ODqye4,1069
20
+ icu4py-0.2.0.dist-info/sboms/auditwheel.cdx.json,sha256=Pvus5d_wQ3covm3v7IhF6fs9Fc6dT8wDuywQJU6eYQo,2237
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp314-cp314-manylinux_2_26_i686
5
+ Tag: cp314-cp314-manylinux_2_28_i686
6
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Johnson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ {"bomFormat": "CycloneDX", "specVersion": "1.4", "version": 1, "metadata": {"component": {"type": "library", "bom-ref": "pkg:pypi/icu4py@0.2.0?file_name=icu4py-0.2.0-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", "name": "icu4py", "version": "0.2.0", "purl": "pkg:pypi/icu4py@0.2.0?file_name=icu4py-0.2.0-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl"}, "tools": [{"name": "auditwheel", "version": "6.6.0"}]}, "components": [{"type": "library", "bom-ref": "pkg:pypi/icu4py@0.2.0?file_name=icu4py-0.2.0-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", "name": "icu4py", "version": "0.2.0", "purl": "pkg:pypi/icu4py@0.2.0?file_name=icu4py-0.2.0-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl"}, {"type": "library", "bom-ref": "pkg:rpm/almalinux/libicu@60.3-2.el8_1#a4dd07e1764f749ca92700550e7aaf73d8e17b38cda7e4393fd076540922ca9b", "name": "libicu", "version": "60.3-2.el8_1", "purl": "pkg:rpm/almalinux/libicu@60.3-2.el8_1"}, {"type": "library", "bom-ref": "pkg:rpm/almalinux/libicu@60.3-2.el8_1#bc54d08b934be90f631e6b943c30a50fbf2f62c31d8ca683d163f18bb541b9c2", "name": "libicu", "version": "60.3-2.el8_1", "purl": "pkg:rpm/almalinux/libicu@60.3-2.el8_1"}, {"type": "library", "bom-ref": "pkg:rpm/almalinux/libicu@60.3-2.el8_1#c024d6fbcf17732e226533e2a8382a4432252af6288c495547593dba68b5d8e1", "name": "libicu", "version": "60.3-2.el8_1", "purl": "pkg:rpm/almalinux/libicu@60.3-2.el8_1"}], "dependencies": [{"ref": "pkg:pypi/icu4py@0.2.0?file_name=icu4py-0.2.0-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", "dependsOn": ["pkg:rpm/almalinux/libicu@60.3-2.el8_1#a4dd07e1764f749ca92700550e7aaf73d8e17b38cda7e4393fd076540922ca9b", "pkg:rpm/almalinux/libicu@60.3-2.el8_1#bc54d08b934be90f631e6b943c30a50fbf2f62c31d8ca683d163f18bb541b9c2", "pkg:rpm/almalinux/libicu@60.3-2.el8_1#c024d6fbcf17732e226533e2a8382a4432252af6288c495547593dba68b5d8e1"]}, {"ref": "pkg:rpm/almalinux/libicu@60.3-2.el8_1#a4dd07e1764f749ca92700550e7aaf73d8e17b38cda7e4393fd076540922ca9b"}, {"ref": "pkg:rpm/almalinux/libicu@60.3-2.el8_1#bc54d08b934be90f631e6b943c30a50fbf2f62c31d8ca683d163f18bb541b9c2"}, {"ref": "pkg:rpm/almalinux/libicu@60.3-2.el8_1#c024d6fbcf17732e226533e2a8382a4432252af6288c495547593dba68b5d8e1"}]}
@@ -0,0 +1 @@
1
+ icu4py
Binary file
Binary file
Binary file