[In subversion/bindings/swig] * core.i (%typemap(in) (const char *data, apr_size_t *len): On Python 3, allow Str as well 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 on Python 3 - Don't raise TypeError exception because all callers don't check it (make_string_from_ob_maybe_null): New function same as make_string_from_ob() but allows None input represents NULL value and raise TypeError if input value don't have appropriate type (svn_swig_py_proparray_from_dict, svn_swig_py_proparray_from_dict): Include acceptable type in TypeError exception message on Python 3 (svn_swig_py_unwrap_string): - Allow Str as well as bytes for source argument on Python 3 (svn_swig_py_make_file): - Allow Str as well as bytes for py_file argument as file path on Python 3 (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.test_char_ptr_in): New test case (SubversionTypemapTestCase.test_char_ptr_may_be_null): New test case (SubversionTypemapTestCase.test_make_string_from_ob): New test case (SubversionTypemapTestCase.test_prophash_from_dict_null_value): New test case, not activate until make specification clear diff --git a/subversion/bindings/swig/core.i b/subversion/bindings/swig/core.i index c309d48070..6993ef4c9c 100644 --- a/subversion/bindings/swig/core.i +++ b/subversion/bindings/swig/core.i @@ -442,18 +442,31 @@ */ #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 IS_PY3 + else if (PyStr_Check($input)) { + $1 = PyStr_AsUTF8AndSize($input, &length); + if (PyErr_Occurred()) { + SWIG_fail; + } } - if (PyBytes_AsStringAndSize($input, &tmpdata, &length) == -1) { +%#endif + else { + PyErr_SetString(PyExc_TypeError, +%#if IS_PY3 + "expecting a bytes or str object for the buffer" +%#else + "expecting a string for the buffer" +%#endif + ); 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..58cfec30a3 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,32 @@ void svn_swig_py_svn_exception(svn_error_t *error_chain) /*** Helper/Conversion Routines ***/ +/* Function to get char * representation of bytes/str object */ +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); + } +#if IS_PY3 + else if (PyStr_Check(input)) { + retval = (char *)PyStr_AsUTF8(input); + } +#endif + else if (input != Py_None || ! maybe_null) { + PyErr_Format(PyExc_TypeError, +#if IS_PY3 + "%s() argument %s must be bytes or str%s, not %s", +#else + "%s() argument %s must be string%s, not %s", +#endif + 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,29 +566,62 @@ static PyObject *make_ob_error(svn_error_t *err) /***/ +static void svn_swig_py_string_type_exception(int maybe_null) { + PyErr_Format(PyExc_TypeError, +#if IS_PY3 + "not a bytes or a str%s", +#else + "not a string%s", +#endif + 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) { - if (ob == Py_None) - return NULL; - if (! PyBytes_Check(ob)) + /* caller should not expect to raise TypeError: check return value + whether it is NULL or not, if needed */ + if (PyBytes_Check(ob)) { - PyErr_SetString(PyExc_TypeError, "not a bytes object"); - return NULL; + return apr_pstrdup(pool, PyBytes_AsString(ob)); + } +#if IS_PY3 + if (PyStr_Check(ob)) + { + return apr_pstrdup(pool, PyStr_AsUTF8(ob)); } - return apr_pstrdup(pool, PyBytes_AsString(ob)); +#endif + return NULL; } -static svn_string_t *make_svn_string_from_ob(PyObject *ob, apr_pool_t *pool) + +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)) + retval = make_string_from_ob(ob, pool); + if (!retval) { + 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)) { - PyErr_SetString(PyExc_TypeError, "not a bytes object"); - return NULL; + return svn_string_create(PyBytes_AsString(ob), pool); } - return svn_string_create(PyBytes_AsString(ob), pool); +#if IS_PY3 + if (PyStr_Check(ob)) + { + return svn_string_create(PyStr_AsUTF8(ob), pool); + } +#endif + return NULL; } @@ -1136,6 +1195,14 @@ apr_hash_t *svn_swig_py_mergeinfo_from_dict(PyObject *dict, return hash; } +#if IS_PY3 +#define TYPE_ERROR_DICT_STRING \ + "dictionary keys/values aren't bytes or str objects" +#else +#define TYPE_ERROR_DICT_STRING \ + "dictionary keys/values aren't strings" +#endif + apr_array_header_t *svn_swig_py_proparray_from_dict(PyObject *dict, apr_pool_t *pool) { @@ -1164,8 +1231,7 @@ apr_array_header_t *svn_swig_py_proparray_from_dict(PyObject *dict, prop->value = make_svn_string_from_ob(value, pool); if (! (prop->name && prop->value)) { - PyErr_SetString(PyExc_TypeError, - "dictionary keys/values aren't strings"); + PyErr_SetString(PyExc_TypeError, TYPE_ERROR_DICT_STRING); Py_DECREF(keys); return NULL; } @@ -1202,8 +1268,7 @@ apr_hash_t *svn_swig_py_prophash_from_dict(PyObject *dict, svn_string_t *propval = make_svn_string_from_ob(value, pool); if (! (propname && propval)) { - PyErr_SetString(PyExc_TypeError, - "dictionary keys/values aren't strings"); + PyErr_SetString(PyExc_TypeError, TYPE_ERROR_DICT_STRING); Py_DECREF(keys); return NULL; } @@ -1321,8 +1386,27 @@ 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); + } +#if IS_PY3 + else if (PyStr_Check(source)) + { + *ptr_dest = PyStr_AsUTF8(source); + } +#endif + else + { + PyErr_Format(PyExc_TypeError, +#if IS_PY3 + "Expected bytes or str object, %s found", +#else + "Expected string or Unicode object, %s found", +#endif + Py_TYPE(source)->tp_name); + *ptr_dest = NULL; + } if (*ptr_dest != NULL) return 0; else @@ -2546,14 +2630,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 +3754,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..37f0975846 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,10 @@ void svn_swig_py_svn_exception(svn_error_t *err); +/* helper function to get char * representation of bytes/str object */ +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..54c50f569f 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,29 @@ 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'] + 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..0e69602b6a --- /dev/null +++ b/subversion/bindings/swig/python/tests/typemap.py @@ -0,0 +1,108 @@ +# +# +# 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 sys + +IS_PY3 = sys.version_info[0] >= 3 + +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 %s," + " not %s" % ("bytes or str" if IS_PY3 else "string", + 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 %s," + " not %s" % ("bytes or str" if IS_PY3 else "string", + 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 %s," + " not int" % ("bytes or str" if IS_PY3 else "string")) + + def test_char_ptr_may_be_null(self): + """Check %typemap(in) IN_STRING 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 %s or None, not %s" + % ("bytes or str" if IS_PY3 else "string", + self.__class__.__name__)) + + def test_make_string_from_ob(self): + """Check test_make_string_from_ob and test_make_svn_string_from_ob + work correctly""" + target_props = { b'a' : b'foo', + b'b' : 'foo', + 'c' : b'' } + source_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(source_props, target_props), + expected) + + @unittest.skip('incomplete implementation of svn.core.svn_prop_diffs()') + def test_prophash_from_dict_null_value(self): + # FIXME: Current implementation of svn_swig_py_prophash_from_dict() + # does not allow None as prop value, however svn_prop_diffs() allows + target_props = { 'a' : 'foo', + 'b' : 'foo', + 'c' : None } + source_props = { 'a' : None, + 'b' : 'bar', + 'c' : 'baz' } + expected = { b'a' : b'foo', + b'b' : b'foo', + b'c' : None } + self.assertEqual(svn.core.svn_prop_diffs(source_props, target_props), + expected) + +def suite(): + return unittest.defaultTestLoader.loadTestsFromTestCase( + SubversionTypemapTestCase) + +if __name__ == '__main__': + runner = unittest.TextTestRunner() + runner.run(suite())