Patch updated.
In article <CAEZNtZ+SKqiATLwDvzyVf=sulpght7usxhzeawnfextpwsb...@mail.gmail.com>
troycurti...@gmail.com writes:
On Thu, Jan 10, 2019 at 9:50 AM Yasuhito FUTATSUKI
<futat...@yf.bsdclub.org> wrote:
The patch attached modifies 4 kind of input argment translations.
(1) typemap(in) char * (with/without const modifiers); not allow NULL,
typemap(in) const char * MAY_BE_NULL; allows NULL
These had done by using 'parse=' typemap modifier, however there is no
PyArg_Parse() format unit to convert both of str and bytes in py3.
So I make a function svn_swig_py_string_to_cstring() in swigutil_py.c,
and use it in for new typemap definition.
consideration:
* For py2, my patch code uses svn_swig_py_string_to_cstring()
- It isn't allow Unicode for input, however 's' and 'z' format units
PyArg_Parse() Unicode in py2. If it is need to accept Unicode in py2,
it is need to fix. (use svn_swig_py_string_to_cstring() py3 only, or
add code to conversion for py2)
Yes I think you should support Unicode in this case, but it turns out
you are most of the way there. If you just remove the IS_PY3
conditional, it will support unicode! The "PyBytes_*" and "PyStr_*"
functions are wrappers provided by the py3c library. The names point
to the concept that they target, and then map it appropriately in Py2
and Py3. So
PyBytes: Sequence of byte values, e.g. "raw data"
In Py2: str
In Py3: bytes
PyStr: Character data
In Py2: Unicode
In Py3: str
Now it uses PyUnicode_Check() and PyStr_UTF8(), then it works in py2 and py3
without IS_PY3 conditional here.
- Difference of TypeError exception message. Pyrg_Parse() reports
argment by argnum in Python wrapper function. However it can't
determin in typemap definition code, so my patch code uses argment
symbol instead.
This is still left, if we treat it as a regression.
* For py3, it seems to need more kindly Exception message for
UnicodeEncodeError, which can be caused if input str contains surrogate
data (U+DC00 - U+DCFF).
I reconsidered it and abandoned, because of UnicodeEncodeError structure.
It is enought to analyse why it causes.
* test case for UnicodeEncodeError is needed
Added.
(2) in core.i, typemap for svn_stream_write
consideration:
* As this typemap doesn't seems to accept Unicode on py2, there seems to
be no regression like described in (1)
* As this typemap only used for svn_stream_write wrapper which have only
one char * arg, default UnicodeEncodeError message seems to be sufficient.
Not changed in this patch.
(3) typemap(in) apr_hash_t * (for various types)
These are using make_string_from_ob() for hash key string conversion,
and typemap(in) apr_hash_t *HASH_CSTRING uses it for hash value
conversion, too. Similarly typemap(in) apr_hash_t *PROPHASH uses
make_svn_strinf_from_ob() for hash value conversion
consideration:
* It seems some of API (e.g. svn_prop_diffs()) allows NULL for hash value,
but current implementation of conversion function doesn't allows. Isn't
it needed? (I added test for this case, but disabled until it make clear)
* test case for UnicodeEncodeError is needed (for both of hash key and
hash value)
Yes this looks like a good catch, the generic conversion function
should handle the NULL/None case.
I've fixed it. To fix this, check key and value separately in
svn_swig_py_*_from_dict().
Test case for UnicodeEncodeError is added, and it detect
make_svn_strinf_from_ob()
couldn't handle this case :) Yes, I've fixed it, of course.
(4) typemap(in) apr_array_header_t *STRINGLIST
This typemap is using svn_swig_py_unwrap_string() through the function
svn_swig_py_seq_to_array().
consideration:
* test case for UnicodeEncodeError is needed (for both of hash key and
hash value)
Test case for UnicodeEncodeError is added in tests/client.py.
And here is the start of my patch review:
I've intended to fix problems pointed out, except the comment for
svn_swig_py_string_to_cstring(). For svn_swig_py_string_to_cstring(),
I've added a comment for the reason of return type and why it seems to
sufficient not to duplicate string entity.
Thanks,
--
Yasuhito FUTATSUKI
On branch swig-py3: accept both of bytes/str for input char * arguments
* Replace typemap(in) for char * using 'parse' modifier with one using
function allows both of bytes/str in py2/py3 in libsvn_swig_py library
* Fix functions to convert Python objects into char pointer to accept
both of bytes/str object in py2/py3
* Fix to accept None as representation of NULL value on conversion
prop dict into apr_hash_t * and apr_array_header_t * of svn_props_t *
[In subversion/bindings/swig]
* core.i (%typemap(in) (const char *data, apr_size_t *len): Allow str
as well as bytes for data argment of svn_stream_write()
* include/svn_global.swg
(remove)(%typemap(in) char *, char const *, char * const,
char const * const): Move this typemap into include/svn_strings as
typemap (in) IN_STRING
* include/svn_string.swg (new)(%typemap(in) IN_STRING): replacement of
%typemap(in) char *, char const *, char * const, char const * const.
actual processing code is moved new svn_swig_py_string_to_cstring()
function in python/libsvn_swig_py/swigutil_py.c
* include/svn_types.swg (%typemap(in) const char *MAY_BE_NULL):
Move processing code into new svn_swig_py_string_to_cstring() function
in python/libsvn_swig_py/swigutil_py.c
* python/libsvn_swig_py/swigutil_py.c
(svn_swig_py_string_to_cstring): New function to convert Python
bytes or str into const char *, with better TypeError exception message
(svn_swig_py_string_type_exception): New function to construct
TypeError exception for new make_string_from_ob_maybe_null() function
(make_string_from_ob, make_svn_string_from_ob):
- Allow str as well as bytes for ob
- Don't raise TypeError exception because all callers don't expect it
(make_string_from_ob_maybe_null, make_svn_string_from_ob_maybe_null):
New function same as make_string_from_ob() and make_svn_string_fromob()
but allows None input represents NULL value and raise TypeError
if input value don't have appropriate type
(svn_swig_py_stringhash_from_dict, svn_swig_py_mergeinfo_from_dict,
svn_swig_py_proparray_from_dict, svn_swig_py_prophash_from_dict,
svn_swig_py_path_rev_hash_from_dict,
svn_swig_py_struct_ptr_hash_from_dict): separate check of key conversion
result and value conversion result
(svn_swig_py_proparray_from_dict, svn_swig_py_prophash_from_dict):
allow NULL for prop values
(svn_swig_py_unwrap_string):
- Allow str as well as bytes for source argument
(svn_swig_py_make_file):
- Allow str as well as bytes for py_file argument as file path
(svn_swig_py_auth_gnome_keyring_unlock_prompt_func):
- Use new function make_string_from_ob_maybe_null() instead of
make_string_from_ob() to check TypeError
- Report Python exception caused by Python callback function as
callback exception error
* python/libsvn_swig_py/swigutil_py.h
Expose new public function make_string_from_ob_maybe_null(), which is
used by typemap(in) char IN_STRING, typemap(in) const char *MAY_BY_NULL
* python/tests/client.py
(SubversionClientTestCase.log_entry_receiver_whole): New helper
callback function for new SubversionClientTestCase.test_log5_revprops
(SubversionClientTestCase.test_log5_revprops): new test for
typemap(in) apr_array_t *STRINGLIST and its helper function
svn_swig_py_unwrap_string()
* python/tests/core.py
(SubversionCoreTestCase.test_stream_write_exception):
- As unicode input is now valid, use int value as invalid input
- (Only for Python 3) Denote that input including surrogate causes
UnicodeEncodeError
(SubversionCoreTestCase.test_stream_write_str):(Only for Python 3)
New test case for svn_stream_write() to pass Str object as data argument
(SubversionCoreTestCase.test_stream_write_bytes):
Renamed from SubversionCoreTestCase.test_stream_write
* python/tests/run_all.py: Register new test module typemap
* python/tests/typemap.py: New unittest module for typemaps
(SubversionTypemapTestCase): New unittest subclass for unit test about
typemaps
(SubversionTypemapTestCase.test_char_ptr_in): New test case
(SubversionTypemapTestCase.test_char_ptr_in_unicode_exception):
New test case(Only for Python3)
(SubversionTypemapTestCase.test_char_ptr_may_be_null): New test case
(SubversionTypemapTestCase.test_char_ptr_may_be_null_unicode_exception):
New test case(Only for Python3)
(SubversionTypemapTestCase.test_make_string_from_ob): New test case
(SubversionTypemapTestCase.test_prophash_from_dict_null_value):
New test case
(SubversionTypemapTestCase.test_make-string_ob_unicode_exception):
New test case(Only for Python3)
(SubversionTypemapTestCase.test_make_svn_string_ob_unicode_exception):
New test case(Only for Python3)
(suite): New function to drive SubversionTypemapTestCase
Review by: troycurtisjr (anticipated)
diff --git a/subversion/bindings/swig/core.i b/subversion/bindings/swig/core.i
index c309d48070..bb94713154 100644
--- a/subversion/bindings/swig/core.i
+++ b/subversion/bindings/swig/core.i
@@ -442,18 +442,24 @@
*/
#ifdef SWIGPYTHON
%typemap(in) (const char *data, apr_size_t *len) ($*2_type temp) {
- char *tmpdata;
Py_ssize_t length;
- if (!PyBytes_Check($input)) {
- PyErr_SetString(PyExc_TypeError,
- "expecting a bytes object for the buffer");
- SWIG_fail;
+ if (PyBytes_Check($input)) {
+ if (PyBytes_AsStringAndSize($input, (char **)&$1, &length) == -1) {
+ SWIG_fail;
+ }
}
- if (PyBytes_AsStringAndSize($input, &tmpdata, &length) == -1) {
+ else if (PyUnicode_Check($input)) {
+ $1 = (char *)PyStr_AsUTF8AndSize($input, &length);
+ if (PyErr_Occurred()) {
+ SWIG_fail;
+ }
+ }
+ else {
+ PyErr_SetString(PyExc_TypeError,
+ "expecting a bytes or str object for the buffer");
SWIG_fail;
}
temp = ($*2_type)length;
- $1 = tmpdata;
$2 = ($2_ltype)&temp;
}
#endif
diff --git a/subversion/bindings/swig/include/svn_global.swg
b/subversion/bindings/swig/include/svn_global.swg
index f7364be28f..57b1e236db 100644
--- a/subversion/bindings/swig/include/svn_global.swg
+++ b/subversion/bindings/swig/include/svn_global.swg
@@ -142,13 +142,6 @@ static PyObject * _global_py_pool = NULL;
/* Python format specifiers. Use Python instead of SWIG to parse
these basic types, because Python reports better error messages
(with correct argument numbers). */
-#if defined(PY3)
-%typemap (in, parse="y")
- char *, char const *, char * const, char const * const "";
-#else
-%typemap (in, parse="s")
- char *, char const *, char * const, char const * const "";
-#endif
%typemap (in, parse="c") char "";
%typemap (in, fragment=SWIG_As_frag(long)) long
diff --git a/subversion/bindings/swig/include/svn_string.swg
b/subversion/bindings/swig/include/svn_string.swg
index 0fc64ebdcc..8be4c3d746 100644
--- a/subversion/bindings/swig/include/svn_string.swg
+++ b/subversion/bindings/swig/include/svn_string.swg
@@ -251,6 +251,26 @@ typedef struct svn_string_t svn_string_t;
}
#endif
+ /* -----------------------------------------------------------------------
+ Type: char * (input)
+*/
+#ifdef SWIGPYTHON
+%typemap (in) IN_STRING
+{
+ $1 = svn_swig_py_string_to_cstring($input, FALSE, "$symname", "$1_name");
+ if (PyErr_Occurred()) SWIG_fail;
+}
+
+%typemap (freearg) IN_STRING "";
+
+%apply IN_STRING {
+ const char *,
+ char *,
+ char const *,
+ char * const,
+ char const * const
+};
+#endif
/* -----------------------------------------------------------------------
define a way to return a 'const char *'
*/
diff --git a/subversion/bindings/swig/include/svn_types.swg
b/subversion/bindings/swig/include/svn_types.swg
index 319f7daa6b..7c933b1ac7 100644
--- a/subversion/bindings/swig/include/svn_types.swg
+++ b/subversion/bindings/swig/include/svn_types.swg
@@ -348,12 +348,8 @@ svn_ ## TYPE ## _swig_rb_closed(VALUE self)
#ifdef SWIGPYTHON
%typemap(in) const char *MAY_BE_NULL
{
- if ($input == Py_None) {
- $1 = NULL;
- } else {
- $1 = PyBytes_AsString($input);
- if ($1 == NULL) SWIG_fail;
- }
+ $1 = svn_swig_py_string_to_cstring($input, TRUE, "$symname", "$1_name");
+ if (PyErr_Occurred()) SWIG_fail;
}
#endif
diff --git a/subversion/bindings/swig/python/libsvn_swig_py/swigutil_py.c
b/subversion/bindings/swig/python/libsvn_swig_py/swigutil_py.c
index 8a4ec631be..e661bca95b 100644
--- a/subversion/bindings/swig/python/libsvn_swig_py/swigutil_py.c
+++ b/subversion/bindings/swig/python/libsvn_swig_py/swigutil_py.c
@@ -506,6 +506,36 @@ void svn_swig_py_svn_exception(svn_error_t *error_chain)
/*** Helper/Conversion Routines ***/
+/* Function to get char * representation of bytes/str object. This is
+ the replacement of typemap(in, parse="s") and typemap(in, parse="z")
+ to accept both of bytes object and str object, and it assumes to be
+ used from those typemaps only.
+ Note: type of return value should be char const *, however, as SWIG
+ produces variables for C function without 'const' modifier, to avoid
+ ton of cast in SWIG produced C code we drop it from return value
+ types as well */
+char *svn_swig_py_string_to_cstring(PyObject *input, int maybe_null,
+ const char * funcsym, const char * argsym)
+{
+ char *retval = NULL;
+ if (PyBytes_Check(input))
+ {
+ retval = PyBytes_AsString(input);
+ }
+ else if (PyUnicode_Check(input))
+ {
+ retval = (char *)PyStr_AsUTF8(input);
+ }
+ else if (input != Py_None || ! maybe_null)
+ {
+ PyErr_Format(PyExc_TypeError,
+ "%s() argument %s must be bytes or str%s, not %s",
+ funcsym, argsym, maybe_null?" or None":"",
+ Py_TYPE(input)->tp_name);
+ }
+ return retval;
+}
+
/* Functions for making Python wrappers around Subversion structs */
static PyObject *make_ob_pool(void *pool)
{
@@ -540,32 +570,89 @@ static PyObject *make_ob_error(svn_error_t *err)
/***/
+static void svn_swig_py_string_type_exception(int maybe_null)
+{
+ PyErr_Format(PyExc_TypeError, "not a bytes or a str%s",
+ maybe_null?" or None":"");
+}
+
/* Conversion from Python single objects (not hashes/lists/etc.) to
Subversion types. */
static char *make_string_from_ob(PyObject *ob, apr_pool_t *pool)
{
+ /* caller should not expect to raise TypeError: check return value
+ whether it is NULL or not, if needed */
+ if (PyBytes_Check(ob))
+ {
+ return apr_pstrdup(pool, PyBytes_AsString(ob));
+ }
+ if (PyUnicode_Check(ob))
+ {
+ /* PyStr_AsUTF8() may cause UnicodeEncodeError,
+ but apr_pstrdup() allows NULL for s */
+ return apr_pstrdup(pool, PyStr_AsUTF8(ob));
+ }
+ return NULL;
+}
+
+static char *make_string_from_ob_maybe_null(PyObject *ob, apr_pool_t *pool)
+{
+ char * retval;
if (ob == Py_None)
- return NULL;
- if (! PyBytes_Check(ob))
{
- PyErr_SetString(PyExc_TypeError, "not a bytes object");
return NULL;
}
- return apr_pstrdup(pool, PyBytes_AsString(ob));
+ retval = make_string_from_ob(ob, pool);
+ if (!retval)
+ {
+ if (!PyErr_Occurred())
+ {
+ svn_swig_py_string_type_exception(TRUE);
+ }
+ }
+ return retval;
}
+
static svn_string_t *make_svn_string_from_ob(PyObject *ob, apr_pool_t *pool)
{
+ /* caller should not expect to raise TypeError: check return value
+ whether it is NULL or not, if needed */
+ if (PyBytes_Check(ob))
+ {
+ return svn_string_create(PyBytes_AsString(ob), pool);
+ }
+ if (PyUnicode_Check(ob))
+ {
+ /* PyStr_AsUTF8() may cause UnicodeEncodeError,
+ and svn_string_create() does not allows NULL for cstring */
+ const char *obstr = PyStr_AsUTF8(ob);
+ if (obstr)
+ {
+ return svn_string_create(obstr, pool);
+ }
+ }
+ return NULL;
+}
+
+static svn_string_t *make_svn_string_from_ob_maybe_null(PyObject *ob,
+ apr_pool_t *pool)
+{
+ svn_string_t * retval;
if (ob == Py_None)
- return NULL;
- if (! PyBytes_Check(ob))
{
- PyErr_SetString(PyExc_TypeError, "not a bytes object");
return NULL;
}
- return svn_string_create(PyBytes_AsString(ob), pool);
+ retval = make_svn_string_from_ob(ob, pool);
+ if (!retval)
+ {
+ if (!PyErr_Occurred())
+ {
+ svn_swig_py_string_type_exception(TRUE);
+ }
+ }
+ return retval;
}
-
/***/
static PyObject *convert_hash(apr_hash_t *hash,
@@ -1055,6 +1142,9 @@ PyObject
*svn_swig_py_changed_path2_hash_to_dict(apr_hash_t *hash)
return dict;
}
+#define TYPE_ERROR_DICT_STRING_KEY \
+ "dictionary keys aren't bytes or str objects"
+
apr_hash_t *svn_swig_py_stringhash_from_dict(PyObject *dict,
apr_pool_t *pool)
{
@@ -1079,11 +1169,18 @@ apr_hash_t *svn_swig_py_stringhash_from_dict(PyObject
*dict,
PyObject *key = PyList_GetItem(keys, i);
PyObject *value = PyDict_GetItem(dict, key);
const char *propname = make_string_from_ob(key, pool);
- const char *propval = make_string_from_ob(value, pool);
- if (! (propname && propval))
+ if (!propname)
+ {
+ if (!PyErr_Occurred())
+ {
+ PyErr_SetString(PyExc_TypeError, TYPE_ERROR_DICT_STRING_KEY);
+ }
+ Py_DECREF(keys);
+ return NULL;
+ }
+ const char *propval = make_string_from_ob_maybe_null(value, pool);
+ if (PyErr_Occurred())
{
- PyErr_SetString(PyExc_TypeError,
- "dictionary keys/values aren't strings");
Py_DECREF(keys);
return NULL;
}
@@ -1116,6 +1213,15 @@ apr_hash_t *svn_swig_py_mergeinfo_from_dict(PyObject
*dict,
PyObject *key = PyList_GetItem(keys, i);
PyObject *value = PyDict_GetItem(dict, key);
const char *pathname = make_string_from_ob(key, pool);
+ if (!pathname)
+ {
+ if (!PyErr_Occurred())
+ {
+ PyErr_SetString(PyExc_TypeError, TYPE_ERROR_DICT_STRING_KEY);
+ }
+ Py_DECREF(keys);
+ return NULL;
+ }
const svn_rangelist_t *ranges = svn_swig_py_seq_to_array(value,
sizeof(const svn_merge_range_t *),
svn_swig_py_unwrap_struct_ptr,
@@ -1123,10 +1229,10 @@ apr_hash_t *svn_swig_py_mergeinfo_from_dict(PyObject
*dict,
pool
);
- if (! (pathname && ranges))
+ if (!ranges)
{
PyErr_SetString(PyExc_TypeError,
- "dictionary keys aren't strings or values aren't
svn_merge_range_t *'s");
+ "dictionary values aren't svn_merge_range_t *'s");
Py_DECREF(keys);
return NULL;
}
@@ -1161,11 +1267,18 @@ apr_array_header_t
*svn_swig_py_proparray_from_dict(PyObject *dict,
PyObject *value = PyDict_GetItem(dict, key);
svn_prop_t *prop = apr_palloc(pool, sizeof(*prop));
prop->name = make_string_from_ob(key, pool);
- prop->value = make_svn_string_from_ob(value, pool);
- if (! (prop->name && prop->value))
+ if (! prop->name)
+ {
+ if (!PyErr_Occurred())
+ {
+ PyErr_SetString(PyExc_TypeError, TYPE_ERROR_DICT_STRING_KEY);
+ }
+ Py_DECREF(keys);
+ return NULL;
+ }
+ prop->value = make_svn_string_from_ob_maybe_null(value, pool);
+ if (PyErr_Occurred())
{
- PyErr_SetString(PyExc_TypeError,
- "dictionary keys/values aren't strings");
Py_DECREF(keys);
return NULL;
}
@@ -1199,11 +1312,18 @@ apr_hash_t *svn_swig_py_prophash_from_dict(PyObject
*dict,
PyObject *key = PyList_GetItem(keys, i);
PyObject *value = PyDict_GetItem(dict, key);
const char *propname = make_string_from_ob(key, pool);
- svn_string_t *propval = make_svn_string_from_ob(value, pool);
- if (! (propname && propval))
+ if (!propname)
+ {
+ if (!PyErr_Occurred())
+ {
+ PyErr_SetString(PyExc_TypeError, TYPE_ERROR_DICT_STRING_KEY);
+ }
+ Py_DECREF(keys);
+ return NULL;
+ }
+ svn_string_t *propval = make_svn_string_from_ob_maybe_null(value, pool);
+ if (PyErr_Occurred())
{
- PyErr_SetString(PyExc_TypeError,
- "dictionary keys/values aren't strings");
Py_DECREF(keys);
return NULL;
}
@@ -1241,8 +1361,10 @@ apr_hash_t
*svn_swig_py_path_revs_hash_from_dict(PyObject *dict,
if (!(path))
{
- PyErr_SetString(PyExc_TypeError,
- "dictionary keys aren't strings");
+ if (!PyErr_Occurred())
+ {
+ PyErr_SetString(PyExc_TypeError, TYPE_ERROR_DICT_STRING_KEY);
+ }
Py_DECREF(keys);
return NULL;
}
@@ -1296,8 +1418,10 @@ apr_hash_t
*svn_swig_py_struct_ptr_hash_from_dict(PyObject *dict,
if (!c_key)
{
- PyErr_SetString(PyExc_TypeError,
- "dictionary keys aren't strings");
+ if (!PyErr_Occurred())
+ {
+ PyErr_SetString(PyExc_TypeError, TYPE_ERROR_DICT_STRING_KEY);
+ }
Py_DECREF(keys);
return NULL;
}
@@ -1321,8 +1445,21 @@ svn_swig_py_unwrap_string(PyObject *source,
void *baton)
{
const char **ptr_dest = destination;
- *ptr_dest = PyBytes_AsString(source);
-
+ if (PyBytes_Check(source))
+ {
+ *ptr_dest = PyBytes_AsString(source);
+ }
+ else if (PyUnicode_Check(source))
+ {
+ *ptr_dest = PyStr_AsUTF8(source);
+ }
+ else
+ {
+ PyErr_Format(PyExc_TypeError,
+ "Expected bytes or str object, %s found",
+ Py_TYPE(source)->tp_name);
+ *ptr_dest = NULL;
+ }
if (*ptr_dest != NULL)
return 0;
else
@@ -1494,7 +1631,7 @@ commit_item_array_to_list(const apr_array_header_t *array)
}
-
+
/*** Errors ***/
/* Convert a given SubversionException to an svn_error_t. On failure returns
@@ -2546,14 +2683,25 @@ apr_file_t *svn_swig_py_make_file(PyObject *py_file,
{
apr_file_t *apr_file = NULL;
apr_status_t apr_err;
+ const char* fname = NULL;
if (py_file == NULL || py_file == Py_None)
return NULL;
+ /* check if input is a path */
if (PyBytes_Check(py_file))
+ {
+ fname = PyBytes_AsString(py_file);
+ }
+#if IS_PY3
+ else if (PyStr_Check(py_file))
+ {
+ fname = PyStr_AsUTF8(py_file);
+ }
+#endif
+ if (fname)
{
/* input is a path -- just open an apr_file_t */
- const char* fname = PyBytes_AsString(py_file);
apr_err = apr_file_open(&apr_file, fname,
APR_CREATE | APR_READ | APR_WRITE,
APR_OS_DEFAULT, pool);
@@ -3659,7 +3807,11 @@ svn_swig_py_auth_gnome_keyring_unlock_prompt_func(char
**keyring_passwd,
}
else
{
- *keyring_passwd = make_string_from_ob(result, pool);
+ *keyring_passwd = make_string_from_ob_maybe_null(result, pool);
+ if (PyErr_Occurred())
+ {
+ err = callback_exception_error();
+ }
Py_DECREF(result);
}
diff --git a/subversion/bindings/swig/python/libsvn_swig_py/swigutil_py.h
b/subversion/bindings/swig/python/libsvn_swig_py/swigutil_py.h
index 89e7c53c28..149ed81ff5 100644
--- a/subversion/bindings/swig/python/libsvn_swig_py/swigutil_py.h
+++ b/subversion/bindings/swig/python/libsvn_swig_py/swigutil_py.h
@@ -107,6 +107,17 @@ void svn_swig_py_svn_exception(svn_error_t *err);
+/* Function to get char * representation of bytes/str object. This is
+ the replacement of typemap(in, parse="s") and typemap(in, parse="z")
+ to accept both of bytes object and str object, and it assumes to be
+ used from those typemaps only.
+ Note: type of return value should be char const *, however, as SWIG
+ produces variables for C function without 'const' modifier, to avoid
+ ton of cast in SWIG produced C code we drop it from return value
+ types as well */
+char *svn_swig_py_string_to_cstring(PyObject *input, int maybe_null,
+ const char * funcsym, const char * argsym);
+
/* helper function to convert an apr_hash_t* (char* -> svnstring_t*) to
a Python dict */
PyObject *svn_swig_py_prophash_to_dict(apr_hash_t *hash);
diff --git a/subversion/bindings/swig/python/tests/client.py
b/subversion/bindings/swig/python/tests/client.py
index 88f7fcf5e4..cd9444cc7f 100644
--- a/subversion/bindings/swig/python/tests/client.py
+++ b/subversion/bindings/swig/python/tests/client.py
@@ -52,6 +52,10 @@ class SubversionClientTestCase(unittest.TestCase):
"""An implementation of svn_log_entry_receiver_t."""
self.received_revisions.append(log_entry.revision)
+ def log_entry_receiver_whole(self, log_entry, pool):
+ """An implementation of svn_log_entry_receiver_t, holds whole log
entries."""
+ self.received_log_entries.append(log_entry)
+
def setUp(self):
"""Set up authentication and client context"""
self.client_ctx = client.svn_client_create_context()
@@ -243,6 +247,33 @@ class SubversionClientTestCase(unittest.TestCase):
self.assertEqual(self.received_revisions, list(range(0, 5)))
+ def test_log5_revprops(self):
+ """Test svn_client_log5 revprops (for typemap(in) apr_array_t
*STRINGLIST)"""
+ directory = urljoin(self.repos_uri+b"/", b"trunk/dir1")
+ start = core.svn_opt_revision_t()
+ end = core.svn_opt_revision_t()
+ core.svn_opt_parse_revision(start, end, b"4:0")
+ rev_range = core.svn_opt_revision_range_t()
+ rev_range.start = start
+ rev_range.end = end
+
+ self.received_log_entries = []
+
+ # (Python 3: pass tuple of bytes and str mixture as revprops argment)
+ client.log5((directory,), start, (rev_range,), 1, True, False, False,
+ ('svn:author', b'svn:log'),
+ self.log_entry_receiver_whole, self.client_ctx)
+ self.assertEqual(len(self.received_log_entries), 1)
+ revprops = self.received_log_entries[0].revprops
+ self.assertEqual(revprops[b'svn:log'], b"More directories.")
+ self.assertEqual(revprops[b'svn:author'], b"john")
+ with self.assertRaises(KeyError):
+ commit_date = revprops['svn:date']
+ with self.assertRaises(UnicodeEncodeError):
+ client.log5((directory,), start, (rev_range,), 1, True, False, False,
+ (u'svn:\udc61uthor', b'svn:log'),
+ self.log_entry_receiver_whole, self.client_ctx)
+
def test_uuid_from_url(self):
"""Test svn_client_uuid_from_url on a file:// URL"""
self.assertTrue(isinstance(
diff --git a/subversion/bindings/swig/python/tests/core.py
b/subversion/bindings/swig/python/tests/core.py
index 6370fa5e1e..9bd82d6535 100644
--- a/subversion/bindings/swig/python/tests/core.py
+++ b/subversion/bindings/swig/python/tests/core.py
@@ -21,6 +21,9 @@
import unittest
import os
import tempfile
+import sys
+
+IS_PY3 = sys.version_info[0] >= 3
import svn.core, svn.client
import utils
@@ -220,13 +223,52 @@ class SubversionCoreTestCase(unittest.TestCase):
svn.core.svn_stream_close(stream)
def test_stream_write_exception(self):
- ostr_unicode = b'Python'.decode()
stream = svn.core.svn_stream_empty()
with self.assertRaises(TypeError):
- svn.core.svn_stream_write(stream, ostr_unicode)
+ svn.core.svn_stream_write(stream, 16)
+ # Check UnicodeEncodeError which can be caused only in Python 3
+ if IS_PY3:
+ o1_str = b'Python\x00\xa4\xd1\xa4\xa4\xa4\xbd\xa4\xf3\r\n'
+ ostr_unicode = o1_str.decode('ascii', 'surrogateescape')
+ with self.assertRaises(UnicodeEncodeError):
+ svn.core.svn_stream_write(stream, ostr_unicode)
svn.core.svn_stream_close(stream)
- def test_stream_write(self):
+ @unittest.skipUnless(IS_PY3, "test for Python 3 only")
+ def test_stream_write_str(self):
+ o1_str = 'Python\x00\xa4\xd1\xa4\xa4\xa4\xbd\xa4\xf3\r\n'
+ o2_str = ('subVersioN\x00'
+ '\xa4\xb5\xa4\xd6\xa4\xd0\xa1\xbc\xa4\xb8\xa4\xe7\xa4\xf3\n')
+ o3_str = 'swig\x00\xa4\xb9\xa4\xa6\xa4\xa3\xa4\xb0\rend'
+ out_str = o1_str + o2_str + o3_str
+ rewrite_str = 'Subversion'
+ fd, fname = tempfile.mkstemp()
+ os.close(fd)
+ try:
+ stream = svn.core.svn_stream_from_aprfile2(fname, False)
+ self.assertEqual(svn.core.svn_stream_write(stream, out_str),
+ len(out_str.encode('UTF-8')))
+ svn.core.svn_stream_seek(stream, None)
+ self.assertEqual(svn.core.svn_stream_read_full(stream, 4096),
+ out_str.encode('UTF-8'))
+ svn.core.svn_stream_seek(stream, None)
+ svn.core.svn_stream_skip(stream, len(o1_str.encode('UTF-8')))
+ self.assertEqual(svn.core.svn_stream_write(stream, rewrite_str),
+ len(rewrite_str.encode('UTF-8')))
+ svn.core.svn_stream_seek(stream, None)
+ self.assertEqual(
+ svn.core.svn_stream_read_full(stream, 4096),
+ (o1_str + rewrite_str
+ + o2_str[len(rewrite_str.encode('UTF-8')):]
+ + o3_str ).encode('UTF-8'))
+ svn.core.svn_stream_close(stream)
+ finally:
+ try:
+ os.remove(fname)
+ except OSError:
+ pass
+
+ def test_stream_write_bytes(self):
o1_str = b'Python\x00\xa4\xd1\xa4\xa4\xa4\xbd\xa4\xf3\r\n'
o2_str = (b'subVersioN\x00'
b'\xa4\xb5\xa4\xd6\xa4\xd0\xa1\xbc\xa4\xb8\xa4\xe7\xa4\xf3\n')
diff --git a/subversion/bindings/swig/python/tests/run_all.py
b/subversion/bindings/swig/python/tests/run_all.py
index 5cfd9d7536..3a042e012c 100644
--- a/subversion/bindings/swig/python/tests/run_all.py
+++ b/subversion/bindings/swig/python/tests/run_all.py
@@ -21,7 +21,7 @@
import sys
import unittest, setup_path
import mergeinfo, core, client, delta, checksum, pool, fs, ra, wc, repository,
\
- auth, trac.versioncontrol.tests
+ auth, trac.versioncontrol.tests, typemap
from svn.core import svn_cache_config_get, svn_cache_config_set
# Run all tests
@@ -47,6 +47,7 @@ def suite():
s.addTest(repository.suite())
s.addTest(auth.suite())
s.addTest(trac.versioncontrol.tests.suite())
+ s.addTest(typemap.suite())
return s
if __name__ == '__main__':
diff --git a/subversion/bindings/swig/python/tests/typemap.py
b/subversion/bindings/swig/python/tests/typemap.py
new file mode 100644
index 0000000000..7f6e839176
--- /dev/null
+++ b/subversion/bindings/swig/python/tests/typemap.py
@@ -0,0 +1,133 @@
+#
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+#
+import unittest
+import os
+import tempfile
+
+import svn.core
+
+class SubversionTypemapTestCase(unittest.TestCase):
+ """Test cases for the SWIG typemaps argments and return values transration"""
+
+ def test_char_ptr_in(self):
+ """Check %typemap(in) IN_STRING works correctly"""
+ self.assertEqual(svn.core.svn_path_canonicalize(b'foo'), b'foo')
+ self.assertEqual(svn.core.svn_dirent_join(b'foo', 'bar'), b'foo/bar')
+ with self.assertRaises(TypeError) as cm:
+ svn.core.svn_dirent_join(None, b'bar')
+ self.assertEqual(str(cm.exception),
+ "svn_dirent_join() argument base must be"
+ " bytes or str, not %s" % None.__class__.__name__)
+ with self.assertRaises(TypeError) as cm:
+ svn.core.svn_dirent_join(b'foo', self)
+ self.assertEqual(str(cm.exception),
+ "svn_dirent_join() argument component must be"
+ " bytes or str, not %s" % self.__class__.__name__)
+ with self.assertRaises(TypeError) as cm:
+ svn.core.svn_dirent_join('foo', 10)
+ self.assertEqual(str(cm.exception),
+ "svn_dirent_join() argument component must be"
+ " bytes or str, not int")
+
+ def test_char_ptr_in_unicode_exception(self):
+ """Check %typemap(in) IN_STRING handles unicode encode error correctly"""
+ with self.assertRaises(UnicodeEncodeError):
+ svn.core.svn_dirent_join(b'foo', u'\udc62\udc61\udc72')
+
+ def test_char_ptr_may_be_null(self):
+ """Check %typemap(in) char *MAY_BE_NULL works correctly"""
+ cfg = svn.core.svn_config_create2(False, False)
+ self.assertEqual(svn.core.svn_config_get(cfg, b'foo', b'bar', b'baz'),
+ b'baz')
+ self.assertEqual(svn.core.svn_config_get(cfg, b'foo', b'bar', 'baz'),
+ b'baz')
+ self.assertIsNone(svn.core.svn_config_get(cfg, b'foo', b'bar', None))
+ with self.assertRaises(TypeError) as cm:
+ svn.core.svn_config_get(cfg, b'foo', b'bar', self)
+ self.assertEqual(str(cm.exception),
+ "svn_config_get() argument default_value"
+ " must be bytes or str or None, not %s"
+ % self.__class__.__name__)
+
+ def test_char_ptr_may_be_null_unicode_exception(self):
+ """Check %typemap(in) char *MAY_BE_NULL handles unicode encode error
correctly"""
+ cfg = svn.core.svn_config_create2(False, False)
+ with self.assertRaises(UnicodeEncodeError):
+ svn.core.svn_config_get(cfg, u'f\udc6fo', b'bar', None)
+
+ def test_make_string_from_ob(self):
+ """Check make_string_from_ob and make_svn_string_from_ob work correctly"""
+ source_props = { b'a' : b'foo',
+ b'b' : 'foo',
+ 'c' : b'' }
+ target_props = { b'a' : '',
+ 'b' : 'bar',
+ b'c' : b'baz' }
+ expected = { b'a' : b'',
+ b'b' : b'bar',
+ b'c' : b'baz' }
+ self.assertEqual(svn.core.svn_prop_diffs(target_props, source_props),
+ expected)
+
+ def test_prophash_from_dict_null_value(self):
+ """Check make_svn_string_from_ob_maybe_null work correctly"""
+ source_props = { 'a' : 'foo',
+ 'b' : 'foo',
+ 'c' : None }
+ target_props = { 'a' : None,
+ 'b' : 'bar',
+ 'c' : 'baz' }
+ expected = { b'a' : None,
+ b'b' : b'bar',
+ b'c' : b'baz' }
+ self.assertEqual(svn.core.svn_prop_diffs(target_props, source_props),
+ expected)
+
+ def test_make_string_from_ob_unicode_exception(self):
+ """Check make_string_from_ob handles unicode encode error correctly"""
+ source_props = { b'a' : b'foo',
+ b'b' : u'foo',
+ u'\udc63' : b'' }
+ target_props = { b'a' : u'',
+ u'b' : u'bar',
+ b'c' : b'baz' }
+ with self.assertRaises(UnicodeEncodeError):
+ svn.core.svn_prop_diffs(target_props, source_props)
+
+ def test_make_svn_string_from_ob_unicode_exception(self):
+ """Check make_string_from_ob handles unicode encode error correctly"""
+ source_props = { b'a' : b'foo',
+ b'b' : 'foo',
+ u'c' : b'' }
+ target_props = { b'a' : u'',
+ u'b' : u'b\udc61r',
+ b'c' : b'baz' }
+ with self.assertRaises(UnicodeEncodeError):
+ svn.core.svn_prop_diffs(target_props, source_props)
+
+
+def suite():
+ return unittest.defaultTestLoader.loadTestsFromTestCase(
+ SubversionTypemapTestCase)
+
+if __name__ == '__main__':
+ runner = unittest.TextTestRunner()
+ runner.run(suite())