Continued work on Unicode support; added new test cases for full unicode
support within Python 2.x; move away from character semantics which Oracle is deprecating anyway to byte semantics which should hopefully eliminate the problem with a backend character set of UTF-8.
This commit is contained in:
parent
45faba4a48
commit
2e26d0beb8
47
Connection.c
47
Connection.c
@ -56,7 +56,7 @@ static PyObject *Connection_GetVersion(udt_Connection*, void*);
|
||||
static PyObject *Connection_GetMaxBytesPerCharacter(udt_Connection*, void*);
|
||||
static PyObject *Connection_ContextManagerEnter(udt_Connection*, PyObject*);
|
||||
static PyObject *Connection_ContextManagerExit(udt_Connection*, PyObject*);
|
||||
#ifdef OCI_NLS_CHARSET_MAXBYTESZ
|
||||
#ifndef WITH_UNICODE
|
||||
static PyObject *Connection_GetEncoding(udt_Connection*, void*);
|
||||
static PyObject *Connection_GetNationalEncoding(udt_Connection*, void*);
|
||||
#endif
|
||||
@ -119,7 +119,7 @@ static PyMemberDef g_ConnectionMembers[] = {
|
||||
// declaration of calculated members for Python type "Connection"
|
||||
//-----------------------------------------------------------------------------
|
||||
static PyGetSetDef g_ConnectionCalcMembers[] = {
|
||||
#ifdef OCI_NLS_CHARSET_MAXBYTESZ
|
||||
#ifndef WITH_UNICODE
|
||||
{ "encoding", (getter) Connection_GetEncoding, 0, 0, 0 },
|
||||
{ "nencoding", (getter) Connection_GetNationalEncoding, 0, 0, 0 },
|
||||
#endif
|
||||
@ -364,6 +364,7 @@ static int Connection_SetOCIAttr(
|
||||
PyObject *value, // value to set
|
||||
ub4 *attribute) // OCI attribute type
|
||||
{
|
||||
udt_StringBuffer buffer;
|
||||
sword status;
|
||||
|
||||
// make sure connection is connected
|
||||
@ -371,13 +372,15 @@ static int Connection_SetOCIAttr(
|
||||
return -1;
|
||||
|
||||
// set the value in the OCI
|
||||
if (!PyString_Check(value)) {
|
||||
if (!CXORA_STRING_CHECK(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "value must be a string");
|
||||
return -1;
|
||||
}
|
||||
if (StringBuffer_Fill(&buffer, value))
|
||||
return -1;
|
||||
status = OCIAttrSet(self->sessionHandle, OCI_HTYPE_SESSION,
|
||||
PyString_AS_STRING(value), PyString_GET_SIZE(value),
|
||||
*attribute, self->environment->errorHandle);
|
||||
buffer.ptr, buffer.size, *attribute,
|
||||
self->environment->errorHandle);
|
||||
if (Environment_CheckForError(self->environment, status,
|
||||
"Connection_SetOCIAttr()") < 0)
|
||||
return -1;
|
||||
@ -786,7 +789,7 @@ static PyObject *Connection_Repr(
|
||||
}
|
||||
|
||||
|
||||
#ifdef OCI_NLS_CHARSET_MAXBYTESZ
|
||||
#ifndef WITH_UNICODE
|
||||
//-----------------------------------------------------------------------------
|
||||
// Connection_GetCharacterSetName()
|
||||
// Retrieve the IANA character set name for the attribute.
|
||||
@ -911,8 +914,8 @@ static PyObject *Connection_GetVersion(
|
||||
udt_Connection *self, // connection object
|
||||
void *arg) // optional argument (ignored)
|
||||
{
|
||||
PyObject *procName, *listOfArguments;
|
||||
udt_Variable *versionVar, *compatVar;
|
||||
PyObject *results, *temp;
|
||||
udt_Cursor *cursor;
|
||||
|
||||
// if version has already been determined, no need to determine again
|
||||
@ -943,29 +946,37 @@ static PyObject *Connection_GetVersion(
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// create the parameters for the function call
|
||||
temp = Py_BuildValue("(s,[OO])",
|
||||
"begin dbms_utility.db_version(:ver, :compat); end;",
|
||||
versionVar, compatVar);
|
||||
// create the list of arguments
|
||||
listOfArguments = PyList_New(2);
|
||||
if (!listOfArguments) {
|
||||
Py_DECREF(versionVar);
|
||||
Py_DECREF(compatVar);
|
||||
if (!temp) {
|
||||
Py_DECREF(cursor);
|
||||
return NULL;
|
||||
}
|
||||
PyList_SET_ITEM(listOfArguments, 0, (PyObject*) versionVar);
|
||||
PyList_SET_ITEM(listOfArguments, 1, (PyObject*) compatVar);
|
||||
|
||||
// create the string variable
|
||||
procName = CXORA_ASCII_TO_STRING("dbms_utility.db_version");
|
||||
if (!procName) {
|
||||
Py_DECREF(listOfArguments);
|
||||
Py_DECREF(cursor);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// execute the cursor
|
||||
results = Cursor_Execute(cursor, temp, NULL);
|
||||
if (!results) {
|
||||
Py_DECREF(temp);
|
||||
// call stored procedure
|
||||
if (Cursor_Call(cursor, NULL, procName, listOfArguments) < 0) {
|
||||
Py_DECREF(procName);
|
||||
Py_DECREF(listOfArguments);
|
||||
Py_DECREF(cursor);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(results);
|
||||
Py_DECREF(procName);
|
||||
|
||||
// retrieve value
|
||||
self->version = Variable_GetValue(versionVar, 0);
|
||||
Py_DECREF(temp);
|
||||
Py_DECREF(listOfArguments);
|
||||
Py_DECREF(cursor);
|
||||
Py_XINCREF(self->version);
|
||||
return self->version;
|
||||
|
||||
43
Cursor.c
43
Cursor.c
@ -204,31 +204,24 @@ static int Cursor_FreeHandle(
|
||||
udt_Cursor *self, // cursor object
|
||||
int raiseException) // raise an exception, if necesary?
|
||||
{
|
||||
ub4 tagLength;
|
||||
udt_StringBuffer buffer;
|
||||
sword status;
|
||||
char *tag;
|
||||
|
||||
if (self->handle) {
|
||||
if (self->isOwned) {
|
||||
OCIHandleFree(self->handle, OCI_HTYPE_STMT);
|
||||
} else {
|
||||
if (self->statementTag) {
|
||||
tag = PyString_AS_STRING(self->statementTag);
|
||||
tagLength = PyString_GET_SIZE(self->statementTag);
|
||||
} else {
|
||||
tag = NULL;
|
||||
tagLength = 0;
|
||||
}
|
||||
if (self->connection->handle != 0) {
|
||||
} else if (self->connection->handle != 0) {
|
||||
if (!StringBuffer_Fill(&buffer, self->statementTag) < 0)
|
||||
return (raiseException) ? -1 : 0;
|
||||
status = OCIStmtRelease(self->handle,
|
||||
self->environment->errorHandle, (text*) tag, tagLength,
|
||||
OCI_DEFAULT);
|
||||
self->environment->errorHandle, (text*) buffer.ptr,
|
||||
buffer.size, OCI_DEFAULT);
|
||||
StringBuffer_CLEAR(&buffer);
|
||||
if (raiseException && Environment_CheckForError(
|
||||
self->environment, status, "Cursor_FreeHandle()") < 0)
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -415,8 +408,7 @@ static int Cursor_GetBindNames(
|
||||
// process the bind information returned
|
||||
for (i = 0; i < foundElements; i++) {
|
||||
if (!duplicate[i]) {
|
||||
temp = PyString_FromStringAndSize(bindNames[i],
|
||||
bindNameLengths[i]);
|
||||
temp = CXORA_BUFFER_TO_STRING(bindNames[i], bindNameLengths[i]);
|
||||
if (!temp) {
|
||||
Py_DECREF(*names);
|
||||
PyMem_Free(buffer);
|
||||
@ -649,14 +641,18 @@ static PyObject *Cursor_ItemDescriptionHelper(
|
||||
type = (PyObject*) varType->pythonType;
|
||||
if (type == (PyObject*) &g_StringVarType)
|
||||
displaySize = internalSize;
|
||||
#ifndef WITH_UNICODE
|
||||
else if (type == (PyObject*) &g_UnicodeVarType)
|
||||
displaySize = internalSize / 2;
|
||||
#endif
|
||||
else if (type == (PyObject*) &g_BinaryVarType)
|
||||
displaySize = internalSize;
|
||||
else if (type == (PyObject*) &g_FixedCharVarType)
|
||||
displaySize = internalSize;
|
||||
#ifndef WITH_UNICODE
|
||||
else if (type == (PyObject*) &g_FixedUnicodeVarType)
|
||||
displaySize = internalSize / 2;
|
||||
#endif
|
||||
else if (type == (PyObject*) &g_NumberVarType) {
|
||||
if (precision) {
|
||||
displaySize = precision + 1;
|
||||
@ -676,7 +672,7 @@ static PyObject *Cursor_ItemDescriptionHelper(
|
||||
return NULL;
|
||||
|
||||
// set each of the items in the tuple
|
||||
PyTuple_SET_ITEM(tuple, 0, CXORA_TO_STRING_OBJ(name, nameLength));
|
||||
PyTuple_SET_ITEM(tuple, 0, CXORA_BUFFER_TO_STRING(name, nameLength));
|
||||
Py_INCREF(type);
|
||||
PyTuple_SET_ITEM(tuple, 1, type);
|
||||
PyTuple_SET_ITEM(tuple, 2, PyInt_FromLong(displaySize));
|
||||
@ -1308,7 +1304,7 @@ static int Cursor_Call(
|
||||
Py_DECREF(arguments);
|
||||
|
||||
// create the statement object
|
||||
format = PyString_FromString(statement);
|
||||
format = PyBytes_FromString(statement);
|
||||
PyMem_Free(statement);
|
||||
if (!format) {
|
||||
Py_DECREF(bindVariables);
|
||||
@ -1436,13 +1432,8 @@ static PyObject *Cursor_Execute(
|
||||
executeArgs = NULL;
|
||||
if (!PyArg_ParseTuple(args, "O|O", &statement, &executeArgs))
|
||||
return NULL;
|
||||
#ifdef WITH_UNICODE
|
||||
if (statement != Py_None && !PyUnicode_Check(statement)) {
|
||||
PyErr_SetString(PyExc_TypeError, "expecting None or unicode string");
|
||||
#else
|
||||
if (statement != Py_None && !PyString_Check(statement)) {
|
||||
if (statement != Py_None && !CXORA_STRING_CHECK(statement)) {
|
||||
PyErr_SetString(PyExc_TypeError, "expecting None or a string");
|
||||
#endif
|
||||
return NULL;
|
||||
}
|
||||
if (executeArgs && keywordArgs) {
|
||||
@ -1520,8 +1511,8 @@ static PyObject *Cursor_ExecuteMany(
|
||||
if (!PyArg_ParseTuple(args, "OO!", &statement, &PyList_Type,
|
||||
&listOfArguments))
|
||||
return NULL;
|
||||
if (statement != Py_None && !PyString_Check(statement)) {
|
||||
PyErr_SetString(PyExc_TypeError, "expecting None or a string");
|
||||
if (statement != Py_None && !CXORA_STRING_CHECK(statement)) {
|
||||
PyErr_SetString(PyExc_TypeError, "expecting None or string");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
@ -75,7 +75,11 @@ static udt_Environment *Environment_New(
|
||||
return NULL;
|
||||
environment->handle = NULL;
|
||||
environment->errorHandle = NULL;
|
||||
#ifdef WITH_UNICODE
|
||||
environment->maxBytesPerCharacter = 2;
|
||||
#else
|
||||
environment->maxBytesPerCharacter = 1;
|
||||
#endif
|
||||
environment->fixedWidth = 1;
|
||||
environment->maxStringBytes = MAX_STRING_CHARS;
|
||||
|
||||
@ -114,7 +118,7 @@ static udt_Environment *Environment_New(
|
||||
}
|
||||
|
||||
// acquire max bytes per character
|
||||
#ifdef OCI_NLS_CHARSET_MAXBYTESZ
|
||||
#ifndef WITH_UNICODE
|
||||
status = OCINlsNumericInfoGet(environment->handle,
|
||||
environment->errorHandle, &environment->maxBytesPerCharacter,
|
||||
OCI_NLS_CHARSET_MAXBYTESZ);
|
||||
|
||||
8
Error.c
8
Error.c
@ -109,7 +109,7 @@ static udt_Error *Error_New(
|
||||
if (errorText[len] == 0 && errorText[len + 1] == 0)
|
||||
break;
|
||||
}
|
||||
error->message = CXORA_TO_STRING_OBJ(errorText, len);
|
||||
error->message = CXORA_BUFFER_TO_STRING(errorText, len);
|
||||
#else
|
||||
error->message = PyString_FromString(errorText);
|
||||
#endif
|
||||
@ -146,10 +146,6 @@ static PyObject *Error_Str(
|
||||
Py_INCREF(self->message);
|
||||
return self->message;
|
||||
}
|
||||
#ifdef WITH_UNICODE
|
||||
return PyUnicode_DecodeASCII("", 0, NULL);
|
||||
#else
|
||||
return PyString_FromString("");
|
||||
#endif
|
||||
return CXORA_ASCII_TO_STRING("");
|
||||
}
|
||||
|
||||
|
||||
19
NumberVar.c
19
NumberVar.c
@ -453,10 +453,10 @@ static PyObject *NumberVar_GetValue(
|
||||
udt_NumberVar *var, // variable to determine value for
|
||||
unsigned pos) // array position
|
||||
{
|
||||
PyObject *result, *stringObj;
|
||||
char stringValue[200];
|
||||
long integerValue;
|
||||
ub4 stringLength;
|
||||
PyObject *result;
|
||||
sword status;
|
||||
|
||||
if (var->type == &vt_Integer || var->type == &vt_Boolean) {
|
||||
@ -473,18 +473,19 @@ static PyObject *NumberVar_GetValue(
|
||||
if (var->type == &vt_NumberAsString || var->type == &vt_LongInteger) {
|
||||
stringLength = sizeof(stringValue);
|
||||
status = OCINumberToText(var->environment->errorHandle,
|
||||
&var->data[pos], (unsigned char*) "TM9", 3, NULL, 0,
|
||||
&stringLength, (unsigned char*) stringValue);
|
||||
&var->data[pos], (text*) g_NumberToStringFormatBuffer.ptr,
|
||||
g_NumberToStringFormatBuffer.size, NULL, 0, &stringLength,
|
||||
(unsigned char*) stringValue);
|
||||
if (Environment_CheckForError(var->environment, status,
|
||||
"NumberVar_GetValue(): as string") < 0)
|
||||
return NULL;
|
||||
stringObj = CXORA_BUFFER_TO_STRING(stringValue, stringLength);
|
||||
if (!stringObj)
|
||||
return NULL;
|
||||
if (var->type == &vt_NumberAsString)
|
||||
return PyString_FromStringAndSize(stringValue, stringLength);
|
||||
result = PyInt_FromString(stringValue, NULL, 10);
|
||||
if (result || !PyErr_ExceptionMatches(PyExc_ValueError))
|
||||
return result;
|
||||
PyErr_Clear();
|
||||
result = PyLong_FromString(stringValue, NULL, 10);
|
||||
return stringObj;
|
||||
result = PyNumber_Int(stringObj);
|
||||
Py_DECREF(stringObj);
|
||||
if (result || !PyErr_ExceptionMatches(PyExc_ValueError))
|
||||
return result;
|
||||
PyErr_Clear();
|
||||
|
||||
@ -169,12 +169,13 @@ static int SessionPool_Init(
|
||||
PyObject *args, // arguments
|
||||
PyObject *keywordArgs) // keyword arguments
|
||||
{
|
||||
unsigned usernameLength, passwordLength, dsnLength, poolNameLength;
|
||||
unsigned minSessions, maxSessions, sessionIncrement;
|
||||
PyObject *threadedObj, *eventsObj, *homogeneousObj;
|
||||
const char *username, *password, *dsn, *poolName;
|
||||
udt_StringBuffer username, password, dsn;
|
||||
int threaded, events, homogeneous;
|
||||
PyTypeObject *connectionType;
|
||||
unsigned poolNameLength;
|
||||
const char *poolName;
|
||||
sword status;
|
||||
ub4 poolMode;
|
||||
ub1 getMode;
|
||||
@ -190,11 +191,11 @@ static int SessionPool_Init(
|
||||
threadedObj = eventsObj = homogeneousObj = NULL;
|
||||
connectionType = &g_ConnectionType;
|
||||
getMode = OCI_SPOOL_ATTRVAL_NOWAIT;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "s#s#s#iii|OObOO",
|
||||
keywordList, &username, &usernameLength, &password,
|
||||
&passwordLength, &dsn, &dsnLength, &minSessions, &maxSessions,
|
||||
&sessionIncrement, &connectionType, &threadedObj, &getMode,
|
||||
&eventsObj, &homogeneousObj))
|
||||
if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "O!O!O!iii|OObOO",
|
||||
keywordList, CXORA_STRING_TYPE, &self->username,
|
||||
CXORA_STRING_TYPE, &self->password, CXORA_STRING_TYPE, &self->dsn,
|
||||
&minSessions, &maxSessions, &sessionIncrement, &connectionType,
|
||||
&threadedObj, &getMode, &eventsObj, &homogeneousObj))
|
||||
return -1;
|
||||
if (!PyType_Check(connectionType)) {
|
||||
PyErr_SetString(g_ProgrammingErrorException,
|
||||
@ -225,6 +226,9 @@ static int SessionPool_Init(
|
||||
// initialize the object's members
|
||||
Py_INCREF(connectionType);
|
||||
self->connectionType = connectionType;
|
||||
Py_INCREF(self->dsn);
|
||||
Py_INCREF(self->username);
|
||||
Py_INCREF(self->password);
|
||||
self->minSessions = minSessions;
|
||||
self->maxSessions = maxSessions;
|
||||
self->sessionIncrement = sessionIncrement;
|
||||
@ -235,21 +239,6 @@ static int SessionPool_Init(
|
||||
if (!self->environment)
|
||||
return -1;
|
||||
|
||||
// create the string for the username
|
||||
self->username = PyString_FromStringAndSize(username, usernameLength);
|
||||
if (!self->username)
|
||||
return -1;
|
||||
|
||||
// create the string for the password
|
||||
self->password = PyString_FromStringAndSize(password, passwordLength);
|
||||
if (!self->password)
|
||||
return -1;
|
||||
|
||||
// create the string for the TNS entry
|
||||
self->dsn = PyString_FromStringAndSize(dsn, dsnLength);
|
||||
if (!self->dsn)
|
||||
return -1;
|
||||
|
||||
// create the session pool handle
|
||||
status = OCIHandleAlloc(self->environment->handle, (dvoid**) &self->handle,
|
||||
OCI_HTYPE_SPOOL, 0, 0);
|
||||
@ -263,19 +252,34 @@ static int SessionPool_Init(
|
||||
poolMode |= OCI_SPC_HOMOGENEOUS;
|
||||
|
||||
// create the session pool
|
||||
if (StringBuffer_Fill(&username, self->username) < 0)
|
||||
return -1;
|
||||
if (StringBuffer_Fill(&password, self->password) < 0) {
|
||||
StringBuffer_CLEAR(&username);
|
||||
return -1;
|
||||
}
|
||||
if (StringBuffer_Fill(&dsn, self->dsn) < 0) {
|
||||
StringBuffer_CLEAR(&username);
|
||||
StringBuffer_CLEAR(&password);
|
||||
return -1;
|
||||
}
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
status = OCISessionPoolCreate(self->environment->handle,
|
||||
self->environment->errorHandle, self->handle,
|
||||
(OraText**) &poolName, &poolNameLength, (OraText*) dsn, dsnLength,
|
||||
minSessions, maxSessions, sessionIncrement, (OraText*) username,
|
||||
usernameLength, (OraText*) password, passwordLength, poolMode);
|
||||
(OraText**) &poolName, &poolNameLength, (OraText*) dsn.ptr,
|
||||
dsn.size, minSessions, maxSessions, sessionIncrement,
|
||||
(OraText*) username.ptr, username.size, (OraText*) password.ptr,
|
||||
password.size, poolMode);
|
||||
Py_END_ALLOW_THREADS
|
||||
StringBuffer_CLEAR(&username);
|
||||
StringBuffer_CLEAR(&password);
|
||||
StringBuffer_CLEAR(&dsn);
|
||||
if (Environment_CheckForError(self->environment, status,
|
||||
"SessionPool_New(): create pool") < 0)
|
||||
return -1;
|
||||
|
||||
// create the string for the pool name
|
||||
self->name = PyString_FromStringAndSize(poolName, poolNameLength);
|
||||
self->name = CXORA_BUFFER_TO_STRING(poolName, poolNameLength);
|
||||
if (!self->name)
|
||||
return -1;
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
// Defines constants and routines specific to handling strings.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// define structure for abstracting string buffers
|
||||
typedef struct {
|
||||
char *ptr;
|
||||
Py_ssize_t size;
|
||||
@ -12,6 +13,19 @@ typedef struct {
|
||||
} udt_StringBuffer;
|
||||
|
||||
|
||||
// use the bytes methods in cx_Oracle and define them as the equivalent string
|
||||
// type methods as is done in Python 2.6
|
||||
#ifndef PyBytes_Check
|
||||
#define PyBytes_Type PyString_Type
|
||||
#define PyBytes_AS_STRING PyString_AS_STRING
|
||||
#define PyBytes_GET_SIZE PyString_GET_SIZE
|
||||
#define PyBytes_Check PyString_Check
|
||||
#define PyBytes_Format PyString_Format
|
||||
#define PyBytes_FromString PyString_FromString
|
||||
#define PyBytes_FromStringAndSize PyString_FromStringAndSize
|
||||
#endif
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// StringBuffer_Fill()
|
||||
// Fill the string buffer with the UTF-16 data that Oracle expects.
|
||||
@ -36,15 +50,15 @@ static int StringBuffer_Fill(
|
||||
PyUnicode_GET_SIZE(obj), NULL, byteOrder);
|
||||
if (!buf->encodedString)
|
||||
return -1;
|
||||
buf->ptr = PyString_AS_STRING(buf->encodedString);
|
||||
buf->size = PyString_GET_SIZE(buf->encodedString);
|
||||
buf->ptr = PyBytes_AS_STRING(buf->encodedString);
|
||||
buf->size = PyBytes_GET_SIZE(buf->encodedString);
|
||||
#else
|
||||
buf->ptr = (char*) PyUnicode_AS_UNICODE(obj);
|
||||
buf->size = PyUnicode_GET_DATA_SIZE(obj);
|
||||
#endif
|
||||
#else
|
||||
buf->ptr = PyString_AS_STRING(obj);
|
||||
buf->size = PyString_GET_SIZE(obj);
|
||||
buf->ptr = PyBytes_AS_STRING(obj);
|
||||
buf->size = PyBytes_GET_SIZE(obj);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
@ -55,23 +69,29 @@ static int StringBuffer_Fill(
|
||||
#define CXORA_ERROR_TEXT_LENGTH 2048
|
||||
#define CXORA_STRING_TYPE &PyUnicode_Type
|
||||
#define CXORA_STRING_FROM_FORMAT PyUnicode_Format
|
||||
#define CXORA_STRING_CHECK PyUnicode_Check
|
||||
#define CXORA_ASCII_TO_STRING(str) \
|
||||
PyUnicode_DecodeASCII(str, strlen(str), NULL)
|
||||
#ifdef Py_UNICODE_WIDE
|
||||
#define StringBuffer_CLEAR(buffer) \
|
||||
Py_XDECREF((buffer)->encodedString)
|
||||
#define CXORA_TO_STRING_OBJ(buffer, numBytes) \
|
||||
#define CXORA_BUFFER_TO_STRING(buffer, numBytes) \
|
||||
PyUnicode_DecodeUTF16(buffer, numBytes, NULL, NULL)
|
||||
#else
|
||||
#define StringBuffer_CLEAR(buffer)
|
||||
#define CXORA_TO_STRING_OBJ(buffer, numBytes) \
|
||||
#define CXORA_BUFFER_TO_STRING(buffer, numBytes) \
|
||||
PyUnicode_FromUnicode((Py_UNICODE*) (buffer), (numBytes) / 2)
|
||||
#endif
|
||||
#else
|
||||
#define CXORA_CHARSETID 0
|
||||
#define CXORA_ERROR_TEXT_LENGTH 1024
|
||||
#define CXORA_STRING_TYPE &PyString_Type
|
||||
#define CXORA_STRING_FROM_FORMAT PyString_Format
|
||||
#define CXORA_STRING_TYPE &PyBytes_Type
|
||||
#define CXORA_STRING_FROM_FORMAT PyBytes_Format
|
||||
#define CXORA_STRING_CHECK PyBytes_Check
|
||||
#define StringBuffer_CLEAR(buffer)
|
||||
#define CXORA_TO_STRING_OBJ(buffer, numBytes) \
|
||||
PyString_FromStringAndSize(buffer, numBytes)
|
||||
#define CXORA_ASCII_TO_STRING(str) \
|
||||
PyBytes_FromString(str)
|
||||
#define CXORA_BUFFER_TO_STRING(buffer, numBytes) \
|
||||
PyBytes_FromStringAndSize(buffer, numBytes)
|
||||
#endif
|
||||
|
||||
|
||||
39
StringVar.c
39
StringVar.c
@ -16,7 +16,9 @@ typedef struct {
|
||||
// Declaration of string variable functions.
|
||||
//-----------------------------------------------------------------------------
|
||||
static int StringVar_Initialize(udt_StringVar*, udt_Cursor*);
|
||||
#ifndef WITH_UNICODE
|
||||
static int StringVar_PostDefine(udt_StringVar*);
|
||||
#endif
|
||||
static int StringVar_SetValue(udt_StringVar*, unsigned, PyObject*);
|
||||
static PyObject *StringVar_GetValue(udt_StringVar*, unsigned);
|
||||
|
||||
@ -48,6 +50,7 @@ static PyTypeObject g_StringVarType = {
|
||||
};
|
||||
|
||||
|
||||
#ifndef WITH_UNICODE
|
||||
static PyTypeObject g_UnicodeVarType = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
"cx_Oracle.UNICODE", // tp_name
|
||||
@ -71,6 +74,7 @@ static PyTypeObject g_UnicodeVarType = {
|
||||
Py_TPFLAGS_DEFAULT, // tp_flags
|
||||
0 // tp_doc
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
static PyTypeObject g_FixedCharVarType = {
|
||||
@ -98,6 +102,7 @@ static PyTypeObject g_FixedCharVarType = {
|
||||
};
|
||||
|
||||
|
||||
#ifndef WITH_UNICODE
|
||||
static PyTypeObject g_FixedUnicodeVarType = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
"cx_Oracle.FIXED_UNICODE", // tp_name
|
||||
@ -121,6 +126,7 @@ static PyTypeObject g_FixedUnicodeVarType = {
|
||||
Py_TPFLAGS_DEFAULT, // tp_flags
|
||||
0 // tp_doc
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
static PyTypeObject g_RowidVarType = {
|
||||
@ -194,6 +200,7 @@ static udt_VariableType vt_String = {
|
||||
};
|
||||
|
||||
|
||||
#ifndef WITH_UNICODE
|
||||
static udt_VariableType vt_NationalCharString = {
|
||||
(InitializeProc) StringVar_Initialize,
|
||||
(FinalizeProc) NULL,
|
||||
@ -210,6 +217,7 @@ static udt_VariableType vt_NationalCharString = {
|
||||
1, // can be copied
|
||||
1 // can be in array
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
static udt_VariableType vt_FixedChar = {
|
||||
@ -230,6 +238,7 @@ static udt_VariableType vt_FixedChar = {
|
||||
};
|
||||
|
||||
|
||||
#ifndef WITH_UNICODE
|
||||
static udt_VariableType vt_FixedNationalChar = {
|
||||
(InitializeProc) StringVar_Initialize,
|
||||
(FinalizeProc) NULL,
|
||||
@ -246,6 +255,7 @@ static udt_VariableType vt_FixedNationalChar = {
|
||||
1, // can be copied
|
||||
1 // can be in array
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
static udt_VariableType vt_Rowid = {
|
||||
@ -302,6 +312,7 @@ static int StringVar_Initialize(
|
||||
}
|
||||
|
||||
|
||||
#ifndef WITH_UNICODE
|
||||
//-----------------------------------------------------------------------------
|
||||
// StringVar_PostDefine()
|
||||
// Set the character set information when values are fetched from this
|
||||
@ -329,6 +340,7 @@ static int StringVar_PostDefine(
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -343,7 +355,6 @@ static int StringVar_SetValue(
|
||||
PyObject *encodedString;
|
||||
Py_ssize_t bufferSize;
|
||||
const void *buffer;
|
||||
ub2 actualLength;
|
||||
|
||||
// get the buffer data and size for binding
|
||||
encodedString = NULL;
|
||||
@ -359,7 +370,6 @@ static int StringVar_SetValue(
|
||||
"expecting string or buffer data");
|
||||
return -1;
|
||||
}
|
||||
actualLength = (ub2) bufferSize;
|
||||
} else {
|
||||
if (!PyUnicode_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "expecting unicode data");
|
||||
@ -372,13 +382,12 @@ static int StringVar_SetValue(
|
||||
PyUnicode_GET_SIZE(value), NULL, byteOrder);
|
||||
if (!encodedString)
|
||||
return -1;
|
||||
buffer = PyString_AS_STRING(encodedString);
|
||||
bufferSize = PyString_GET_SIZE(encodedString);
|
||||
buffer = PyBytes_AS_STRING(encodedString);
|
||||
bufferSize = PyBytes_GET_SIZE(encodedString);
|
||||
#else
|
||||
buffer = PyUnicode_AS_UNICODE(value);
|
||||
bufferSize = sizeof(Py_UNICODE) * PyUnicode_GET_SIZE(value);
|
||||
bufferSize = PyUnicode_GET_DATA_SIZE(value);
|
||||
#endif
|
||||
actualLength = bufferSize / 2;
|
||||
}
|
||||
|
||||
// ensure that the buffer is not too large
|
||||
@ -395,7 +404,7 @@ static int StringVar_SetValue(
|
||||
}
|
||||
|
||||
// keep a copy of the string
|
||||
var->actualLength[pos] = actualLength;
|
||||
var->actualLength[pos] = (ub2) bufferSize;
|
||||
if (bufferSize)
|
||||
memcpy(var->data + var->maxLength * pos, buffer, bufferSize);
|
||||
Py_XDECREF(encodedString);
|
||||
@ -415,12 +424,18 @@ static PyObject *StringVar_GetValue(
|
||||
char *data;
|
||||
|
||||
data = var->data + pos * var->maxLength;
|
||||
#ifdef WTIH_UNICODE
|
||||
if (var->type->charsetForm == SQLCS_IMPLICIT)
|
||||
#else
|
||||
#ifdef WITH_UNICODE
|
||||
if (var->type == &vt_Binary)
|
||||
return PyBytes_FromStringAndSize(data, var->actualLength[pos]);
|
||||
return CXORA_BUFFER_TO_STRING(data, var->actualLength[pos]);
|
||||
#else
|
||||
if (var->type->charsetForm == SQLCS_IMPLICIT)
|
||||
return PyBytes_FromStringAndSize(data, var->actualLength[pos]);
|
||||
#ifdef Py_UNICODE_WIDE
|
||||
return PyUnicode_DecodeUTF16(data, var->actualLength[pos], NULL, NULL);
|
||||
#else
|
||||
return PyUnicode_FromUnicode((Py_UNICODE*) data, var->actualLength[pos]);
|
||||
#endif
|
||||
#endif
|
||||
return PyString_FromStringAndSize(data, var->actualLength[pos]);
|
||||
return CXORA_TO_STRING_OBJ(data, var->actualLength[pos]);
|
||||
}
|
||||
|
||||
|
||||
21
Variable.c
21
Variable.c
@ -332,8 +332,10 @@ static int Variable_Check(
|
||||
Py_TYPE(object) == &g_NumberVarType ||
|
||||
Py_TYPE(object) == &g_StringVarType ||
|
||||
Py_TYPE(object) == &g_FixedCharVarType ||
|
||||
#ifndef WITH_UNICODE
|
||||
Py_TYPE(object) == &g_UnicodeVarType ||
|
||||
Py_TYPE(object) == &g_FixedUnicodeVarType ||
|
||||
#endif
|
||||
Py_TYPE(object) == &g_RowidVarType ||
|
||||
Py_TYPE(object) == &g_BinaryVarType ||
|
||||
Py_TYPE(object) == &g_TimestampVarType
|
||||
@ -355,16 +357,18 @@ static udt_VariableType *Variable_TypeByPythonType(
|
||||
{
|
||||
if (type == (PyObject*) &g_StringVarType)
|
||||
return &vt_String;
|
||||
if (type == (PyObject*) &PyString_Type)
|
||||
if (type == (PyObject*) CXORA_STRING_TYPE)
|
||||
return &vt_String;
|
||||
if (type == (PyObject*) &g_FixedCharVarType)
|
||||
return &vt_FixedChar;
|
||||
#ifndef WITH_UNICODE
|
||||
if (type == (PyObject*) &g_UnicodeVarType)
|
||||
return &vt_NationalCharString;
|
||||
if (type == (PyObject*) &PyUnicode_Type)
|
||||
return &vt_NationalCharString;
|
||||
if (type == (PyObject*) &g_FixedUnicodeVarType)
|
||||
return &vt_FixedNationalChar;
|
||||
#endif
|
||||
if (type == (PyObject*) &g_RowidVarType)
|
||||
return &vt_Rowid;
|
||||
if (type == (PyObject*) &g_BinaryVarType)
|
||||
@ -437,10 +441,12 @@ static udt_VariableType *Variable_TypeByValue(
|
||||
// handle scalars
|
||||
if (value == Py_None)
|
||||
return &vt_String;
|
||||
if (PyString_Check(value))
|
||||
if (CXORA_STRING_CHECK(value))
|
||||
return &vt_String;
|
||||
#ifndef WITH_UNICODE
|
||||
if (PyUnicode_Check(value))
|
||||
return &vt_NationalCharString;
|
||||
#endif
|
||||
if (PyInt_Check(value))
|
||||
return &vt_Integer;
|
||||
if (PyLong_Check(value))
|
||||
@ -503,12 +509,16 @@ static udt_VariableType *Variable_TypeByOracleDataType (
|
||||
case SQLT_LNG:
|
||||
return &vt_LongString;
|
||||
case SQLT_AFC:
|
||||
#ifndef WITH_UNICODE
|
||||
if (charsetForm == SQLCS_NCHAR)
|
||||
return &vt_FixedNationalChar;
|
||||
#endif
|
||||
return &vt_FixedChar;
|
||||
case SQLT_CHR:
|
||||
#ifndef WITH_UNICODE
|
||||
if (charsetForm == SQLCS_NCHAR)
|
||||
return &vt_NationalCharString;
|
||||
#endif
|
||||
return &vt_String;
|
||||
case SQLT_RDD:
|
||||
return &vt_Rowid;
|
||||
@ -1031,8 +1041,8 @@ static int Variable_InternalBind(
|
||||
return -1;
|
||||
|
||||
// set the charset form and id if applicable
|
||||
#ifndef WITH_UNICODE
|
||||
if (var->type->charsetForm != SQLCS_IMPLICIT) {
|
||||
ub4 lengthInChars = var->maxLength / 2;
|
||||
ub2 charsetId = OCI_UTF16ID;
|
||||
status = OCIAttrSet(var->bindHandle, OCI_HTYPE_BIND,
|
||||
(dvoid*) &var->type->charsetForm, 0, OCI_ATTR_CHARSET_FORM,
|
||||
@ -1047,12 +1057,13 @@ static int Variable_InternalBind(
|
||||
"Variable_InternalBind(): setting charset Id") < 0)
|
||||
return -1;
|
||||
status = OCIAttrSet(var->bindHandle, OCI_HTYPE_BIND,
|
||||
(dvoid*) &lengthInChars, 0, OCI_ATTR_CHAR_COUNT,
|
||||
(dvoid*) &var->maxLength, 0, OCI_ATTR_MAXDATA_SIZE,
|
||||
var->environment->errorHandle);
|
||||
if (Environment_CheckForError(var->environment, status,
|
||||
"Variable_InternalBind(): set char count") < 0)
|
||||
"Variable_InternalBind(): set max data size") < 0)
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
// set the max data size for strings
|
||||
if ((var->type == &vt_String || var->type == &vt_FixedChar)
|
||||
|
||||
18
cx_Oracle.c
18
cx_Oracle.c
@ -79,6 +79,7 @@ typedef int Py_ssize_t;
|
||||
#define BUILD_VERSION_STRING xstr(BUILD_VERSION)
|
||||
#define DRIVER_NAME "cx_Oracle-"BUILD_VERSION_STRING
|
||||
|
||||
#include "StringUtils.c"
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Globals
|
||||
@ -98,6 +99,8 @@ static PyObject *g_ProgrammingErrorException = NULL;
|
||||
static PyObject *g_NotSupportedErrorException = NULL;
|
||||
static PyTypeObject *g_DateTimeType = NULL;
|
||||
static PyTypeObject *g_DecimalType = NULL;
|
||||
static PyObject *g_NumberToStringFormatObj = NULL;
|
||||
static udt_StringBuffer g_NumberToStringFormatBuffer;
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -141,7 +144,6 @@ static int GetModuleAndName(
|
||||
}
|
||||
|
||||
|
||||
#include "StringUtils.c"
|
||||
#include "Environment.c"
|
||||
#include "SessionPool.c"
|
||||
|
||||
@ -346,6 +348,14 @@ void initcx_Oracle(void)
|
||||
"Decimal");
|
||||
PyErr_Clear();
|
||||
|
||||
// set up the string and buffer for converting numbers to strings
|
||||
g_NumberToStringFormatObj = CXORA_ASCII_TO_STRING("TM9");
|
||||
if (!g_NumberToStringFormatObj)
|
||||
return;
|
||||
if (StringBuffer_Fill(&g_NumberToStringFormatBuffer,
|
||||
g_NumberToStringFormatObj) < 0)
|
||||
return;
|
||||
|
||||
// prepare the types for use by the module
|
||||
MAKE_TYPE_READY(&g_ConnectionType);
|
||||
MAKE_TYPE_READY(&g_CursorType);
|
||||
@ -374,8 +384,10 @@ void initcx_Oracle(void)
|
||||
MAKE_VARIABLE_TYPE_READY(&g_BFILEVarType);
|
||||
MAKE_VARIABLE_TYPE_READY(&g_CursorVarType);
|
||||
MAKE_VARIABLE_TYPE_READY(&g_ObjectVarType);
|
||||
#ifndef WITH_UNICODE
|
||||
MAKE_VARIABLE_TYPE_READY(&g_UnicodeVarType);
|
||||
MAKE_VARIABLE_TYPE_READY(&g_FixedUnicodeVarType);
|
||||
#endif
|
||||
#ifdef SQLT_BFLOAT
|
||||
MAKE_VARIABLE_TYPE_READY(&g_NativeFloatVarType);
|
||||
#endif
|
||||
@ -442,7 +454,10 @@ void initcx_Oracle(void)
|
||||
ADD_TYPE_OBJECT("OBJECT", &g_ObjectVarType)
|
||||
ADD_TYPE_OBJECT("DATETIME", &g_DateTimeVarType)
|
||||
ADD_TYPE_OBJECT("FIXED_CHAR", &g_FixedCharVarType)
|
||||
#ifndef WITH_UNICODE
|
||||
ADD_TYPE_OBJECT("FIXED_UNICODE", &g_FixedUnicodeVarType)
|
||||
ADD_TYPE_OBJECT("UNICODE", &g_UnicodeVarType)
|
||||
#endif
|
||||
ADD_TYPE_OBJECT("LOB", &g_ExternalLobVarType)
|
||||
ADD_TYPE_OBJECT("LONG_BINARY", &g_LongBinaryVarType)
|
||||
ADD_TYPE_OBJECT("LONG_STRING", &g_LongStringVarType)
|
||||
@ -451,7 +466,6 @@ void initcx_Oracle(void)
|
||||
ADD_TYPE_OBJECT("ROWID", &g_RowidVarType)
|
||||
ADD_TYPE_OBJECT("STRING", &g_StringVarType)
|
||||
ADD_TYPE_OBJECT("TIMESTAMP", &g_TimestampVarType)
|
||||
ADD_TYPE_OBJECT("UNICODE", &g_UnicodeVarType)
|
||||
#ifdef SQLT_BFLOAT
|
||||
ADD_TYPE_OBJECT("NATIVE_FLOAT", &g_NativeFloatVarType)
|
||||
#endif
|
||||
|
||||
@ -2,13 +2,16 @@
|
||||
|
||||
import cx_Oracle
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
def GetValue(name, label):
|
||||
value = os.environ.get("CX_ORACLE_" + name)
|
||||
if value is None:
|
||||
value = raw_input(label + ": ")
|
||||
if hasattr(cx_Oracle, "UNICODE") or sys.version_info[0] >= 3:
|
||||
return value
|
||||
return unicode(value)
|
||||
|
||||
USERNAME = GetValue("USERNAME", "user name")
|
||||
PASSWORD = GetValue("PASSWORD", "password")
|
||||
|
||||
@ -10,6 +10,7 @@ print "Running tests for cx_Oracle version", cx_Oracle.version
|
||||
|
||||
import TestEnv
|
||||
|
||||
if hasattr(cx_Oracle, "UNICODE") or sys.version_info[0] >= 3:
|
||||
moduleNames = [
|
||||
"Connection",
|
||||
"Cursor",
|
||||
@ -24,6 +25,10 @@ moduleNames = [
|
||||
"TimestampVar",
|
||||
"UnicodeVar"
|
||||
]
|
||||
else:
|
||||
moduleNames = [
|
||||
"uConnection"
|
||||
]
|
||||
|
||||
class BaseTestCase(unittest.TestCase):
|
||||
|
||||
|
||||
116
test/uConnection.py
Normal file
116
test/uConnection.py
Normal file
@ -0,0 +1,116 @@
|
||||
"""Module for testing connections."""
|
||||
|
||||
import threading
|
||||
|
||||
class TestConnection(TestCase):
|
||||
|
||||
def __ConnectAndDrop(self):
|
||||
"""Connect to the database, perform a query and drop the connection."""
|
||||
connection = cx_Oracle.connect(self.username, self.password,
|
||||
self.tnsentry, threaded = True)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(u"select count(*) from TestNumbers")
|
||||
count, = cursor.fetchone()
|
||||
self.failUnlessEqual(count, 10)
|
||||
|
||||
def setUp(self):
|
||||
self.username = USERNAME
|
||||
self.password = PASSWORD
|
||||
self.tnsentry = TNSENTRY
|
||||
|
||||
def verifyArgs(self, connection):
|
||||
self.failUnlessEqual(connection.username, self.username,
|
||||
"user name differs")
|
||||
self.failUnlessEqual(connection.password, self.password,
|
||||
"password differs")
|
||||
self.failUnlessEqual(connection.tnsentry, self.tnsentry,
|
||||
"tnsentry differs")
|
||||
|
||||
def testAllArgs(self):
|
||||
"connection to database with user, password, TNS separate"
|
||||
connection = cx_Oracle.connect(self.username, self.password,
|
||||
self.tnsentry)
|
||||
self.verifyArgs(connection)
|
||||
|
||||
def testBadConnectString(self):
|
||||
"connection to database with bad connect string"
|
||||
self.failUnlessRaises(cx_Oracle.DatabaseError, cx_Oracle.connect,
|
||||
self.username)
|
||||
self.failUnlessRaises(cx_Oracle.DatabaseError, cx_Oracle.connect,
|
||||
self.username + u"@" + self.tnsentry)
|
||||
self.failUnlessRaises(cx_Oracle.DatabaseError, cx_Oracle.connect,
|
||||
self.username + u"@" + self.tnsentry + u"/" + self.password)
|
||||
|
||||
def testBadPassword(self):
|
||||
"connection to database with bad password"
|
||||
self.failUnlessRaises(cx_Oracle.DatabaseError, cx_Oracle.connect,
|
||||
self.username, self.password + u"X", self.tnsentry)
|
||||
|
||||
def testExceptionOnClose(self):
|
||||
"confirm an exception is raised after closing a connection"
|
||||
connection = cx_Oracle.connect(self.username, self.password,
|
||||
self.tnsentry)
|
||||
connection.close()
|
||||
self.failUnlessRaises(cx_Oracle.InterfaceError, connection.rollback)
|
||||
|
||||
def testMakeDSN(self):
|
||||
"test making a data source name from host, port and sid"
|
||||
formatString = u"(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=" \
|
||||
u"(PROTOCOL=TCP)(HOST=%s)(PORT=%d)))(CONNECT_DATA=(SID=%s)))"
|
||||
args = (u"hostname", 1521, u"TEST")
|
||||
result = cx_Oracle.makedsn(*args)
|
||||
self.failUnlessEqual(result, formatString % args)
|
||||
|
||||
def testSingleArg(self):
|
||||
"connection to database with user, password, TNS together"
|
||||
connection = cx_Oracle.connect(u"%s/%s@%s" % \
|
||||
(self.username, self.password, self.tnsentry))
|
||||
self.verifyArgs(connection)
|
||||
|
||||
def testVersion(self):
|
||||
"connection version is a string"
|
||||
connection = cx_Oracle.connect(self.username, self.password,
|
||||
self.tnsentry)
|
||||
self.failUnless(isinstance(connection.version, unicode))
|
||||
|
||||
def testRollbackOnClose(self):
|
||||
"connection rolls back before close"
|
||||
connection = cx_Oracle.connect(self.username, self.password,
|
||||
self.tnsentry)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(u"truncate table TestExecuteMany")
|
||||
otherConnection = cx_Oracle.connect(self.username, self.password,
|
||||
self.tnsentry)
|
||||
otherCursor = otherConnection.cursor()
|
||||
otherCursor.execute(u"insert into TestExecuteMany values (1)")
|
||||
otherConnection.close()
|
||||
cursor.execute(u"select count(*) from TestExecuteMany")
|
||||
count, = cursor.fetchone()
|
||||
self.failUnlessEqual(count, 0)
|
||||
|
||||
def testRollbackOnDel(self):
|
||||
"connection rolls back before destruction"
|
||||
connection = cx_Oracle.connect(self.username, self.password,
|
||||
self.tnsentry)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(u"truncate table TestExecuteMany")
|
||||
otherConnection = cx_Oracle.connect(self.username, self.password,
|
||||
self.tnsentry)
|
||||
otherCursor = otherConnection.cursor()
|
||||
otherCursor.execute(u"insert into TestExecuteMany values (1)")
|
||||
del otherCursor
|
||||
del otherConnection
|
||||
cursor.execute(u"select count(*) from TestExecuteMany")
|
||||
count, = cursor.fetchone()
|
||||
self.failUnlessEqual(count, 0)
|
||||
|
||||
def testThreading(self):
|
||||
"connection to database with multiple threads"
|
||||
threads = []
|
||||
for i in range(20):
|
||||
thread = threading.Thread(None, self.__ConnectAndDrop)
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user