Skip to content

Commit 9a8cc66

Browse files
committed
1. Fixed encoding/decoding for legacy swig versions by implementing a new auto-generated python module for decoding/encoding
2. Remove replace_swig_import_helper.py 3. Fixed test in get_connection.py by removing usage of "encodeUTF8", which actually was used from the global namespace, and not the lambda in the UDF 4. Fixed missing decodings/encodings in exascript_python_preset_core.py and exascript_python_wrap.py 5. Fixed some errors in python_ext_dataframe.cc
1 parent 9228bd5 commit 9a8cc66

File tree

12 files changed

+114
-85
lines changed

12 files changed

+114
-85
lines changed

exaudfclient/.bazelrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ build:valgrind --copt -O1
2525
build:valgrind -c dbg
2626
build:valgrind --copt -g
2727
build:valgrind --strip=never
28-
build:valgrind --copt -DVALGRIND_ACTIVE
28+
build:valgrind --copt -DVALGRIND_ACTIVE
29+
build:swig-string-as-buffer --copt='-DSWIG_STRING_AS_BYTES_ENABLED=1' --action_env=SWIG_STRING_AS_BYTES_ENABLED=1

exaudfclient/base/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package(default_visibility = ["//visibility:public"])
33
exports_files(["filter_swig_code.py", "build_integrated.py",
44
"create_binary_wrapper.sh", "create_binary_wrapper_valgrind.sh",
55
"create_binary_wrapper_valgrind_massif.sh", "exaudfclient.template.sh",
6-
"replace_swig_import_helper.py"])
6+
"add_encoding_decoding.py"])
77

88
load("//:variables.bzl", "VM_ENABLED_DEFINES")
99

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import os
2+
import sys
3+
from pathlib import Path
4+
5+
CONVERT_TO_BYTES = """
6+
decodeUTF8 = lambda x: x.decode(encoding='utf-8') if isinstance(x, bytes) else x
7+
encodeUTF8 = lambda x: x.encode(encoding='utf-8') if isinstance(x, str) else x
8+
"""
9+
10+
IDENTITY = """
11+
decodeUTF8 = lambda x: x
12+
encodeUTF8 = lambda x: x
13+
"""
14+
15+
def add_encoding_decoding(target):
16+
if "SWIG_STRING_AS_BYTES_ENABLED" in os.environ:
17+
decoding_encdecoding = CONVERT_TO_BYTES
18+
else:
19+
decoding_encdecoding = IDENTITY
20+
21+
with open(target, "wt", encoding="utf-8") as f:
22+
f.write(decoding_encdecoding)
23+
24+
25+
if __name__ == "__main__":
26+
if len(sys.argv) < 2:
27+
print('Usage: add_encoding_decoding.py target')
28+
sys.exit(1)
29+
add_encoding_decoding(sys.argv[1])

exaudfclient/base/python/exascript_python_preset_core.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import sys
22
unicode = str
3-
decodeUTF8 = lambda x: x
4-
encodeUTF8 = lambda x: x
53

64
from exascript_python import *
75
import decimal
@@ -83,14 +81,14 @@ def ci(x, tbl):
8381
colprec = self.__meta.inputColumnPrecision(x)
8482
colscale = self.__meta.inputColumnScale(x)
8583
colsize = self.__meta.inputColumnSize(x)
86-
coltn = self.__meta.inputColumnTypeName(x)
84+
coltn = decodeUTF8(self.__meta.inputColumnTypeName(x))
8785
elif tbl == 'output':
8886
colname = decodeUTF8(self.__meta.outputColumnName(x))
8987
coltype = self.__meta.outputColumnType(x)
9088
colprec = self.__meta.outputColumnPrecision(x)
9189
colscale = self.__meta.outputColumnScale(x)
9290
colsize = self.__meta.outputColumnSize(x)
93-
coltn = self.__meta.outputColumnTypeName(x)
91+
coltn = decodeUTF8(self.__meta.outputColumnTypeName(x))
9492
class exacolumn:
9593
def __init__(self, cn, ct, st, cp, cs, l):
9694
self.name = cn

exaudfclient/base/python/exascript_python_wrap.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
pyextdataframe_pkg = None
44

55
unicode = str
6-
decodeUTF8 = lambda x: x
7-
encodeUTF8 = lambda x: x
86
long = int
97

108
if 'LIBPYEXADATAFRAME_DIR' in os.environ:
@@ -45,10 +43,10 @@ def resget():
4543
return val
4644
return resget
4745
def convert_date(x):
48-
val = datetime.datetime.strptime(x, "%Y-%m-%d")
46+
val = datetime.datetime.strptime(decodeUTF8(x), "%Y-%m-%d")
4947
return datetime.date(val.year, val.month, val.day)
5048
def convert_timestamp(x):
51-
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S.%f")
49+
return datetime.datetime.strptime(decodeUTF8(x), "%Y-%m-%d %H:%M:%S.%f")
5250
self.__incoltypes = []
5351
for col in range(self.__meta.inputColumnCount()):
5452
self.__incoltypes.append(self.__meta.inputColumnType(col))
@@ -67,8 +65,8 @@ def convert_timestamp(x):
6765
data[colname] = rd(inp.getInt64, inp.wasNull, col)
6866
elif self.__incoltypes[col] == NUMERIC:
6967
if self.__meta.inputColumnScale(col) == 0:
70-
data[colname] = rd(inp.getNumeric, inp.wasNull, col, lambda x: int(str(x)))
71-
else: data[colname] = rd(inp.getNumeric, inp.wasNull, col, lambda x: decimal.Decimal(str(x)))
68+
data[colname] = rd(inp.getNumeric, inp.wasNull, col, lambda x: int(decodeUTF8(x)))
69+
else: data[colname] = rd(inp.getNumeric, inp.wasNull, col, lambda x: decimal.Decimal(decodeUTF8(x)))
7270
elif self.__incoltypes[col] == DATE:
7371
data[colname] = rd(inp.getDate, inp.wasNull, col, convert_date)
7472
elif self.__incoltypes[col] == TIMESTAMP:
@@ -138,7 +136,7 @@ def emit(self, *output):
138136
elif type(v) in (int, long):
139137
if self.__outcoltypes[k] == INT32: self.__out.setInt32(k, int(v))
140138
elif self.__outcoltypes[k] == INT64: self.__out.setInt64(k, int(v))
141-
elif self.__outcoltypes[k] == NUMERIC: self.__out.setNumeric(k, str(int(v)))
139+
elif self.__outcoltypes[k] == NUMERIC: self.__out.setNumeric(k, encodeUTF8(str(int(v))))
142140
elif self.__outcoltypes[k] == DOUBLE: self.__out.setDouble(k, float(v))
143141
else:
144142
raise RuntimeError(u"E-UDF-CL-SL-PYTHON-1091: emit column '%s' is of type %s but data given have type %s" \
@@ -147,7 +145,7 @@ def emit(self, *output):
147145
if self.__outcoltypes[k] == DOUBLE: self.__out.setDouble(k, float(v))
148146
elif self.__outcoltypes[k] == INT32: self.__out.setInt32(k, int(v))
149147
elif self.__outcoltypes[k] == INT64: self.__out.setInt64(k, int(v))
150-
elif self.__outcoltypes[k] == NUMERIC: self.__out.setInt64(k, str(v))
148+
elif self.__outcoltypes[k] == NUMERIC: self.__out.setInt64(k, encodeUTF8(str(v)))
151149
else:
152150
raise RuntimeError(u"E-UDF-CL-SL-PYTHON-1092: emit column '%s' is of type %s but data given have type %s" \
153151
% (decodeUTF8(self.__meta.outputColumnName(k)), type_names.get(self.__outcoltypes[k], 'UNKONWN'), str(type(v))))
@@ -156,15 +154,15 @@ def emit(self, *output):
156154
raise RuntimeError(u"E-UDF-CL-SL-PYTHON-1093: emit column '%s' is of type %s but data given have type %s" \
157155
% (decodeUTF8(self.__meta.outputColumnName(k)), type_names.get(self.__outcoltypes[k], 'UNKONWN'), str(type(v))))
158156
self.__out.setBoolean(k, bool(v))
159-
elif type(v) in (str, unicode):
157+
elif type(v) in (str, unicode, bytes):
160158
v = encodeUTF8(v)
161159
vl = len(v)
162160
if self.__outcoltypes[k] != STRING:
163161
raise RuntimeError(u"E-UDF-CL-SL-PYTHON-1094: emit column '%s' is of type %s but data given have type %s" \
164162
% (decodeUTF8(self.__meta.outputColumnName(k)), type_names.get(self.__outcoltypes[k], 'UNKONWN'), str(type(v))))
165163
self.__out.setString(k, v, vl)
166164
elif type(v) == decimal.Decimal:
167-
if self.__outcoltypes[k] == NUMERIC: self.__out.setNumeric(k, str(v))
165+
if self.__outcoltypes[k] == NUMERIC: self.__out.setNumeric(k, encodeUTF8(str(v)))
168166
elif self.__outcoltypes[k] == INT32: self.__out.setInt32(k, int(v))
169167
elif self.__outcoltypes[k] == INT64: self.__out.setInt64(k, int(v))
170168
elif self.__outcoltypes[k] == DOUBLE: self.__out.setDouble(k, float(v))
@@ -175,12 +173,12 @@ def emit(self, *output):
175173
if self.__outcoltypes[k] != DATE:
176174
raise RuntimeError("E-UDF-CL-SL-PYTHON-1096: emit column '%s' is of type %s but data given have type %s" \
177175
% (decodeUTF8(self.__meta.outputColumnName(k)), type_names.get(self.__outcoltypes[k], 'UNKONWN'), str(type(v))))
178-
self.__out.setDate(k, v.isoformat())
176+
self.__out.setDate(k, encodeUTF8(v.isoformat()))
179177
elif type(v) == datetime.datetime:
180178
if self.__outcoltypes[k] != TIMESTAMP:
181179
raise RuntimeError("E-UDF-CL-SL-PYTHON-1097: emit column '%s' is of type %s but data given have type %s" \
182180
% (decodeUTF8(self.__meta.outputColumnName(k)), type_names.get(self.__outcoltypes[k], 'UNKONWN'), str(type(v))))
183-
self.__out.setTimestamp(k, v.isoformat(' '))
181+
self.__out.setTimestamp(k, encodeUTF8(v.isoformat(' ')))
184182
else: raise RuntimeError("E-UDF-CL-SL-PYTHON-1098: data type %s is not supported" % str(type(v)))
185183
msg = self.__out.checkException()
186184
if msg: raise RuntimeError("F-UDF-CL-SL-PYTHON-1099: "+msg)

exaudfclient/base/python/python3/BUILD

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,9 @@ genrule(
88
cp "$(location //base/exaudflib:swig/script_data_transfer_objects_wrapper.h)" "$(location //base/exaudflib:exascript.i)" build_exascript_python_tmp_cc/exaudflib
99
cd build_exascript_python_tmp_cc
1010
swig -v $$INCLUDES -O -DEXTERNAL_PROCESS -Wall -c++ -python -py3 -addextern -module exascript_python -o "../$(location exascript_python_tmp.cc)" exaudflib/exascript.i
11-
cd ..
12-
#python3 "$(location //base:replace_swig_import_helper.py)" "$(location exascript_python.py)" "$(location exascript_python.py)"
1311
""",
1412
outs = ["exascript_python_tmp.cc", "exascript_python.py"],
1513
srcs = ["//base/exaudflib:exascript.i","//base/exaudflib:swig/script_data_transfer_objects_wrapper.h"],
16-
tools = ["//base:replace_swig_import_helper.py"],
1714
)
1815

1916
genrule(
@@ -38,14 +35,23 @@ genrule(
3835
tools = ["//base/python:extend_exascript_python_preset_py.sh"]
3936
)
4037

38+
genrule(
39+
name = "gen_encoding_decoding",
40+
cmd = 'python3 "$(location //base:add_encoding_decoding.py)" "$(location exascript_encoding_decoding.py)"',
41+
outs = ["exascript_encoding_decoding.py"],
42+
srcs = [],
43+
tools = ["//base:add_encoding_decoding.py"]
44+
)
45+
4146
genrule(
4247
name = "exascript_python_int",
4348
cmd = """
4449
cp $(SRCS) .
45-
python3 $(location //base:build_integrated.py) "$(location exascript_python_int.h)" "exascript_python.py" "exascript_python_wrap.py" "exascript_python_preset.py"
50+
python3 $(location //base:build_integrated.py) "$(location exascript_python_int.h)" "exascript_python.py" "exascript_python_wrap.py" "exascript_python_preset.py" "exascript_encoding_decoding.py"
4651
""",
4752
outs = ["exascript_python_int.h"],
48-
srcs = [":exascript_python_tmp_cc", "//base/python:exascript_python_wrap.py", ":extend_exascript_python_preset_py"],
53+
srcs = [":exascript_python_tmp_cc", "//base/python:exascript_python_wrap.py",
54+
":extend_exascript_python_preset_py", ":gen_encoding_decoding"],
4955
tools = ["//base:build_integrated.py"]
5056
)
5157

exaudfclient/base/python/python3/python_ext_dataframe.cc

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,23 @@ inline void checkPyObjIsNotNull(const PyObject *obj) {
175175
throw std::runtime_error("F-UDF-CL-SL-PYTHON-1142");
176176
}
177177

178+
inline PyObject* convertPyStringToPyBytes(PyObject* pyString) {
179+
#ifdef SWIG_STRING_AS_BYTES_ENABLED
180+
return PyObject_CallMethod(pyString, "encode", NULL);
181+
#else
182+
Py_INCREF(pyString);
183+
return pyString;
184+
#endif
185+
}
186+
187+
inline PyObject* convertPyBytesToPyString(PyObject* pyString) {
188+
#ifdef SWIG_STRING_AS_BYTES_ENABLED
189+
return PyObject_CallMethod(pyString, "decode", NULL);
190+
#else
191+
Py_INCREF(pyString);
192+
return pyString;
193+
#endif
194+
}
178195

179196

180197
struct ColumnInfo
@@ -923,8 +940,11 @@ inline void handleEmitPyInt(
923940
break;
924941
}
925942
case SWIGVMContainers::NUMERIC:
926-
pyValue.reset(PyObject_Str(pyInt));
943+
{
944+
PyPtr pyStrInt(PyObject_Str(pyInt));
945+
pyValue.reset(convertPyStringToPyBytes(pyStrInt.get()));
927946
break;
947+
}
928948
case SWIGVMContainers::DOUBLE:
929949
{
930950
double value = PyFloat_AsDouble(pyInt);
@@ -977,8 +997,11 @@ inline void handleEmitPyFloat(
977997
break;
978998
}
979999
case SWIGVMContainers::NUMERIC:
980-
pyValue.reset(PyObject_Str(pyFloat));
1000+
{
1001+
PyPtr pyStrFloat(PyObject_Str(pyFloat));
1002+
pyValue.reset(convertPyStringToPyBytes(pyStrFloat.get()));
9811003
break;
1004+
}
9821005
case SWIGVMContainers::DOUBLE:
9831006
{
9841007
//pyFloat points to a 'borrowed' reference. We need to explicitly increase the ref counter here, as pyValue will decrease it again later.
@@ -1020,15 +1043,21 @@ inline void handleEmitPyDecimal(
10201043
case SWIGVMContainers::INT64:
10211044
case SWIGVMContainers::INT32:
10221045
{
1046+
std::cout << "handleEmitPyDecimal: INT" << std::endl;
10231047
PyPtr pyInt(PyObject_CallMethodObjArgs(pyDecimal, pyIntMethodName.get(), NULL));
10241048
pyValue.reset(pyInt.release());
10251049
break;
10261050
}
10271051
case SWIGVMContainers::NUMERIC:
1028-
pyValue.reset(PyObject_Str(pyDecimal));
1052+
{
1053+
std::cout << "handleEmitPyDecimal: NUMERIC" << std::endl;
1054+
PyPtr pyStrDecimal(PyObject_Str(pyDecimal));
1055+
pyValue.reset(convertPyStringToPyBytes(pyStrDecimal.get()));
10291056
break;
1057+
}
10301058
case SWIGVMContainers::DOUBLE:
10311059
{
1060+
std::cout << "handleEmitPyDecimal: DOUBLE" << std::endl;
10321061
PyPtr pyFloat(PyObject_CallMethodObjArgs(pyDecimal, pyFloatMethodName.get(), NULL));
10331062
pyValue.reset(pyFloat.release());
10341063
break;
@@ -1067,11 +1096,12 @@ inline void handleEmitPyStr(
10671096
case SWIGVMContainers::STRING:
10681097
{
10691098
Py_ssize_t size = -1;
1099+
PyPtr bytesString(convertPyStringToPyBytes(pyString));
10701100
const char *str = PyUnicode_AsUTF8AndSize(pyString, &size);
10711101
if (!str && size < 0)
10721102
throw std::runtime_error("F-UDF-CL-SL-PYTHON-1137: invalid size of string");
10731103
PyPtr pySize(PyLong_FromSsize_t(size));
1074-
pyResult.reset(PyObject_CallMethodObjArgs(resultHandler, pyColSetMethods[c].second.get(), pyColSetMethods[c].first.get(), pyString, pySize.get(), NULL));
1104+
pyResult.reset(PyObject_CallMethodObjArgs(resultHandler, pyColSetMethods[c].second.get(), pyColSetMethods[c].first.get(), bytesString.get(), pySize.get(), NULL));
10751105
break;
10761106
}
10771107
default:
@@ -1105,7 +1135,8 @@ inline void handleEmitPyDate(
11051135
case SWIGVMContainers::DATE:
11061136
{
11071137
PyPtr pyIsoDate(PyObject_CallMethodObjArgs(pyDate, pyIsoformatMethodName.get(), NULL));
1108-
pyResult.reset(PyObject_CallMethodObjArgs(resultHandler, pyColSetMethods[c].second.get(), pyColSetMethods[c].first.get(), pyIsoDate.get(), NULL));
1138+
PyPtr bytesIsoDate(convertPyStringToPyBytes(pyIsoDate.get()));
1139+
pyResult.reset(PyObject_CallMethodObjArgs(resultHandler, pyColSetMethods[c].second.get(), pyColSetMethods[c].first.get(), bytesIsoDate.get(), NULL));
11091140
break;
11101141
}
11111142
default:
@@ -1141,8 +1172,9 @@ inline void handleEmitPyTimestamp(
11411172
// it to the generated string.
11421173
PyPtr pyTzLocalize(PyObject_CallMethod(pyTimestamp, "tz_localize", "z", NULL));
11431174
PyPtr pyIsoDatetime(PyObject_CallMethod(pyTzLocalize.get(), "isoformat", "s", " "));
1175+
PyPtr bytesDateTime(convertPyStringToPyBytes(pyIsoDatetime.get()));
11441176
pyResult.reset(PyObject_CallMethodObjArgs(
1145-
resultHandler, pyColSetMethods[c].second.get(), pyColSetMethods[c].first.get(), pyIsoDatetime.get(), NULL));
1177+
resultHandler, pyColSetMethods[c].second.get(), pyColSetMethods[c].first.get(), bytesDateTime.get(), NULL));
11461178
break;
11471179
}
11481180
default:
@@ -1177,8 +1209,9 @@ inline void handleEmitNpyDateTime(
11771209
case SWIGVMContainers::TIMESTAMP:
11781210
{
11791211
PyPtr pyIsoDatetime(PyObject_CallMethod(pyTimestamp, "isoformat", "s", " "));
1212+
PyPtr bytesDateTime(convertPyStringToPyBytes(pyIsoDatetime.get()));
11801213
pyResult.reset(PyObject_CallMethodObjArgs(
1181-
resultHandler, pyColSetMethods[c].second.get(), pyColSetMethods[c].first.get(), pyIsoDatetime.get(), NULL));
1214+
resultHandler, pyColSetMethods[c].second.get(), pyColSetMethods[c].first.get(), bytesDateTime.get(), NULL));
11821215
break;
11831216
}
11841217
default:
@@ -1377,7 +1410,8 @@ void emit(PyObject *resultHandler, std::vector<ColumnInfo>& colInfo, PyObject *d
13771410

13781411
PyPtr pyCheckException(PyObject_CallMethodObjArgs(resultHandler, pyCheckExceptionMethodName.get(), NULL));
13791412
if (pyCheckException.get() != Py_None) {
1380-
const char *exMsg = PyUnicode_AsUTF8(pyCheckException.get());
1413+
PyPtr exceptionAsPyBytes(convertPyBytesToPyString(pyCheckException.get()));
1414+
const char *exMsg = PyUnicode_AsUTF8(exceptionAsPyBytes.get());
13811415
if (exMsg) {
13821416
std::stringstream ss;
13831417
ss << "F-UDF-CL-SL-PYTHON-1075: emit(): " << exMsg;

exaudfclient/base/python/pythoncontainer.cc

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,16 +194,22 @@ PythonVMImpl::PythonVMImpl(bool checkOnly): m_checkOnly(checkOnly)
194194
check("F-UDF-CL-SL-PYTHON-1010");
195195
if (exatable == NULL) throw PythonVM::exception("F-UDF-CL-SL-PYTHON-1011: Failed to import code module");
196196

197+
code = Py_CompileString(integrated_exascript_encoding_decoding_py, "<EXASCRIPTDECENC>", Py_file_input); check("F-UDF-CL-SL-PYTHON-1143");
198+
if (code == NULL) {check("F-UDF-CL-SL-PYTHON-1144");}
199+
200+
PyEval_EvalCode(code, globals, globals); check("F-UDF-CL-SL-PYTHON-1145");
201+
Py_DECREF(code);
202+
197203
code = Py_CompileString(integrated_exascript_python_preset_py, "<EXASCRIPTPP>", Py_file_input); check("F-UDF-CL-SL-PYTHON-1012");
198204
if (code == NULL) {check("F-UDF-CL-SL-PYTHON-1013");}
199205

200-
PyEval_EvalCode(code, globals, globals); check("F-UDF-CL-SL-PYTHON-1014");
206+
PyEval_EvalCode(code, globals, globals); check("F-UDF-CL-SL-PYTHON-1014");
201207
Py_DECREF(code);
202208

203-
PyObject *runobj = PyDict_GetItemString(globals, "__pythonvm_wrapped_parse"); check("F-UDF-CL-SL-PYTHON-1016");
204-
//PyObject *retvalue = PyObject_CallFunction(runobj, NULL); check();
205-
PyObject *retvalue = PyObject_CallFunctionObjArgs(runobj, globals, NULL); check("F-UDF-CL-SL-PYTHON-1017");
206-
Py_XDECREF(retvalue); retvalue = NULL;
209+
PyObject *runobj = PyDict_GetItemString(globals, "__pythonvm_wrapped_parse"); check("F-UDF-CL-SL-PYTHON-1016");
210+
//PyObject *retvalue = PyObject_CallFunction(runobj, NULL); check();
211+
PyObject *retvalue = PyObject_CallFunctionObjArgs(runobj, globals, NULL); check("F-UDF-CL-SL-PYTHON-1017");
212+
Py_XDECREF(retvalue); retvalue = NULL;
207213

208214
code = Py_CompileString(integrated_exascript_python_wrap_py, "<EXASCRIPT>", Py_file_input); check("F-UDF-CL-SL-PYTHON-1018");
209215
if (code == NULL) throw PythonVM::exception("Failed to compile wrapping script");
@@ -489,6 +495,7 @@ const char* PythonVMImpl::singleCall(single_call_function_id_e fn, const Executi
489495
PyObject* p3str = PyUnicode_AsEncodedString(repr, "utf-8", "ignore");
490496
const char *bytes = PyBytes_AS_STRING(p3str);
491497
singleCallResult = string(bytes);
498+
std::cout << "pythoncontainer: singleCallResult='" << singleCallResult << "'" << std::endl;
492499
Py_XDECREF(retvalue); retvalue = NULL;
493500
return singleCallResult.c_str();
494501
}

0 commit comments

Comments
 (0)