Commit 6166519d authored by Florent Xicluna's avatar Florent Xicluna

Closes #13297: use bytes type to send and receive binary data through XMLRPC.

parent 1d8f3f45
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.. XXX Not everything is documented yet. It might be good to describe .. XXX Not everything is documented yet. It might be good to describe
Marshaller, Unmarshaller, getparser, dumps, loads, and Transport. Marshaller, Unmarshaller, getparser and Transport.
**Source code:** :source:`Lib/xmlrpc/client.py` **Source code:** :source:`Lib/xmlrpc/client.py`
...@@ -21,7 +21,12 @@ supports writing XML-RPC client code; it handles all the details of translating ...@@ -21,7 +21,12 @@ supports writing XML-RPC client code; it handles all the details of translating
between conformable Python objects and XML on the wire. between conformable Python objects and XML on the wire.
.. class:: ServerProxy(uri, transport=None, encoding=None, verbose=False, allow_none=False, use_datetime=False) .. class:: ServerProxy(uri, transport=None, encoding=None, verbose=False, \
allow_none=False, use_datetime=False, \
use_builtin_types=False)
.. versionchanged:: 3.3
The *use_builtin_types* flag was added.
A :class:`ServerProxy` instance is an object that manages communication with a A :class:`ServerProxy` instance is an object that manages communication with a
remote XML-RPC server. The required first argument is a URI (Uniform Resource remote XML-RPC server. The required first argument is a URI (Uniform Resource
...@@ -34,9 +39,13 @@ between conformable Python objects and XML on the wire. ...@@ -34,9 +39,13 @@ between conformable Python objects and XML on the wire.
XML; the default behaviour is for ``None`` to raise a :exc:`TypeError`. This is XML; the default behaviour is for ``None`` to raise a :exc:`TypeError`. This is
a commonly-used extension to the XML-RPC specification, but isn't supported by a commonly-used extension to the XML-RPC specification, but isn't supported by
all clients and servers; see http://ontosys.com/xml-rpc/extensions.php for a all clients and servers; see http://ontosys.com/xml-rpc/extensions.php for a
description. The *use_datetime* flag can be used to cause date/time values to description. The *use_builtin_types* flag can be used to cause date/time values
be presented as :class:`datetime.datetime` objects; this is false by default. to be presented as :class:`datetime.datetime` objects and binary data to be
:class:`datetime.datetime` objects may be passed to calls. presented as :class:`bytes` objects; this flag is false by default.
:class:`datetime.datetime` and :class:`bytes` objects may be passed to calls.
The obsolete *use_datetime* flag is similar to *use_builtin_types* but it
applies only to date/time values.
Both the HTTP and HTTPS transports support the URL syntax extension for HTTP Both the HTTP and HTTPS transports support the URL syntax extension for HTTP
Basic Authentication: ``http://user:pass@host:port/path``. The ``user:pass`` Basic Authentication: ``http://user:pass@host:port/path``. The ``user:pass``
...@@ -78,12 +87,12 @@ between conformable Python objects and XML on the wire. ...@@ -78,12 +87,12 @@ between conformable Python objects and XML on the wire.
| | only their *__dict__* attribute is | | | only their *__dict__* attribute is |
| | transmitted. | | | transmitted. |
+---------------------------------+---------------------------------------------+ +---------------------------------+---------------------------------------------+
| :const:`dates` | in seconds since the epoch (pass in an | | :const:`dates` | In seconds since the epoch. Pass in an |
| | instance of the :class:`DateTime` class) or | | | instance of the :class:`DateTime` class or |
| | a :class:`datetime.datetime` instance. | | | a :class:`datetime.datetime` instance. |
+---------------------------------+---------------------------------------------+ +---------------------------------+---------------------------------------------+
| :const:`binary data` | pass in an instance of the :class:`Binary` | | :const:`binary data` | Pass in an instance of the :class:`Binary` |
| | wrapper class | | | wrapper class or a :class:`bytes` instance. |
+---------------------------------+---------------------------------------------+ +---------------------------------+---------------------------------------------+
This is the full set of data types supported by XML-RPC. Method calls may also This is the full set of data types supported by XML-RPC. Method calls may also
...@@ -98,8 +107,9 @@ between conformable Python objects and XML on the wire. ...@@ -98,8 +107,9 @@ between conformable Python objects and XML on the wire.
ensure that the string is free of characters that aren't allowed in XML, such as ensure that the string is free of characters that aren't allowed in XML, such as
the control characters with ASCII values between 0 and 31 (except, of course, the control characters with ASCII values between 0 and 31 (except, of course,
tab, newline and carriage return); failing to do this will result in an XML-RPC tab, newline and carriage return); failing to do this will result in an XML-RPC
request that isn't well-formed XML. If you have to pass arbitrary strings via request that isn't well-formed XML. If you have to pass arbitrary bytes
XML-RPC, use the :class:`Binary` wrapper class described below. via XML-RPC, use the :class:`bytes` class or the class:`Binary` wrapper class
described below.
:class:`Server` is retained as an alias for :class:`ServerProxy` for backwards :class:`Server` is retained as an alias for :class:`ServerProxy` for backwards
compatibility. New code should use :class:`ServerProxy`. compatibility. New code should use :class:`ServerProxy`.
...@@ -249,7 +259,7 @@ The client code for the preceding server:: ...@@ -249,7 +259,7 @@ The client code for the preceding server::
Binary Objects Binary Objects
-------------- --------------
This class may be initialized from string data (which may include NULs). The This class may be initialized from bytes data (which may include NULs). The
primary access to the content of a :class:`Binary` object is provided by an primary access to the content of a :class:`Binary` object is provided by an
attribute: attribute:
...@@ -257,15 +267,15 @@ attribute: ...@@ -257,15 +267,15 @@ attribute:
.. attribute:: Binary.data .. attribute:: Binary.data
The binary data encapsulated by the :class:`Binary` instance. The data is The binary data encapsulated by the :class:`Binary` instance. The data is
provided as an 8-bit string. provided as a :class:`bytes` object.
:class:`Binary` objects have the following methods, supported mainly for :class:`Binary` objects have the following methods, supported mainly for
internal use by the marshalling/unmarshalling code: internal use by the marshalling/unmarshalling code:
.. method:: Binary.decode(string) .. method:: Binary.decode(bytes)
Accept a base64 string and decode it as the instance's new data. Accept a base64 :class:`bytes` object and decode it as the instance's new data.
.. method:: Binary.encode(out) .. method:: Binary.encode(out)
...@@ -471,14 +481,21 @@ Convenience Functions ...@@ -471,14 +481,21 @@ Convenience Functions
it via an extension, provide a true value for *allow_none*. it via an extension, provide a true value for *allow_none*.
.. function:: loads(data, use_datetime=False) .. function:: loads(data, use_datetime=False, use_builtin_types=False)
Convert an XML-RPC request or response into Python objects, a ``(params, Convert an XML-RPC request or response into Python objects, a ``(params,
methodname)``. *params* is a tuple of argument; *methodname* is a string, or methodname)``. *params* is a tuple of argument; *methodname* is a string, or
``None`` if no method name is present in the packet. If the XML-RPC packet ``None`` if no method name is present in the packet. If the XML-RPC packet
represents a fault condition, this function will raise a :exc:`Fault` exception. represents a fault condition, this function will raise a :exc:`Fault` exception.
The *use_datetime* flag can be used to cause date/time values to be presented as The *use_builtin_types* flag can be used to cause date/time values to be
:class:`datetime.datetime` objects; this is false by default. presented as :class:`datetime.datetime` objects and binary data to be
presented as :class:`bytes` objects; this flag is false by default.
The obsolete *use_datetime* flag is similar to *use_builtin_types* but it
applies only to date/time values.
.. versionchanged:: 3.3
The *use_builtin_types* flag was added.
.. _xmlrpc-client-example: .. _xmlrpc-client-example:
......
...@@ -24,6 +24,8 @@ alist = [{'astring': 'foo@bar.baz.spam', ...@@ -24,6 +24,8 @@ alist = [{'astring': 'foo@bar.baz.spam',
'ashortlong': 2, 'ashortlong': 2,
'anotherlist': ['.zyx.41'], 'anotherlist': ['.zyx.41'],
'abase64': xmlrpclib.Binary(b"my dog has fleas"), 'abase64': xmlrpclib.Binary(b"my dog has fleas"),
'b64bytes': b"my dog has fleas",
'b64bytearray': bytearray(b"my dog has fleas"),
'boolean': False, 'boolean': False,
'unicode': '\u4000\u6000\u8000', 'unicode': '\u4000\u6000\u8000',
'ukey\u4000': 'regular value', 'ukey\u4000': 'regular value',
...@@ -44,27 +46,54 @@ class XMLRPCTestCase(unittest.TestCase): ...@@ -44,27 +46,54 @@ class XMLRPCTestCase(unittest.TestCase):
def test_dump_bare_datetime(self): def test_dump_bare_datetime(self):
# This checks that an unwrapped datetime.date object can be handled # This checks that an unwrapped datetime.date object can be handled
# by the marshalling code. This can't be done via test_dump_load() # by the marshalling code. This can't be done via test_dump_load()
# since with use_datetime set to 1 the unmarshaller would create # since with use_builtin_types set to 1 the unmarshaller would create
# datetime objects for the 'datetime[123]' keys as well # datetime objects for the 'datetime[123]' keys as well
dt = datetime.datetime(2005, 2, 10, 11, 41, 23) dt = datetime.datetime(2005, 2, 10, 11, 41, 23)
self.assertEqual(dt, xmlrpclib.DateTime('20050210T11:41:23'))
s = xmlrpclib.dumps((dt,)) s = xmlrpclib.dumps((dt,))
(newdt,), m = xmlrpclib.loads(s, use_datetime=1)
result, m = xmlrpclib.loads(s, use_builtin_types=True)
(newdt,) = result
self.assertEqual(newdt, dt) self.assertEqual(newdt, dt)
self.assertEqual(m, None) self.assertIs(type(newdt), datetime.datetime)
self.assertIsNone(m)
result, m = xmlrpclib.loads(s, use_builtin_types=False)
(newdt,) = result
self.assertEqual(newdt, dt)
self.assertIs(type(newdt), xmlrpclib.DateTime)
self.assertIsNone(m)
result, m = xmlrpclib.loads(s, use_datetime=True)
(newdt,) = result
self.assertEqual(newdt, dt)
self.assertIs(type(newdt), datetime.datetime)
self.assertIsNone(m)
result, m = xmlrpclib.loads(s, use_datetime=False)
(newdt,) = result
self.assertEqual(newdt, dt)
self.assertIs(type(newdt), xmlrpclib.DateTime)
self.assertIsNone(m)
(newdt,), m = xmlrpclib.loads(s, use_datetime=0)
self.assertEqual(newdt, xmlrpclib.DateTime('20050210T11:41:23'))
def test_datetime_before_1900(self): def test_datetime_before_1900(self):
# same as before but with a date before 1900 # same as before but with a date before 1900
dt = datetime.datetime(1, 2, 10, 11, 41, 23) dt = datetime.datetime(1, 2, 10, 11, 41, 23)
self.assertEqual(dt, xmlrpclib.DateTime('00010210T11:41:23'))
s = xmlrpclib.dumps((dt,)) s = xmlrpclib.dumps((dt,))
(newdt,), m = xmlrpclib.loads(s, use_datetime=1)
result, m = xmlrpclib.loads(s, use_builtin_types=True)
(newdt,) = result
self.assertEqual(newdt, dt) self.assertEqual(newdt, dt)
self.assertEqual(m, None) self.assertIs(type(newdt), datetime.datetime)
self.assertIsNone(m)
(newdt,), m = xmlrpclib.loads(s, use_datetime=0) result, m = xmlrpclib.loads(s, use_builtin_types=False)
self.assertEqual(newdt, xmlrpclib.DateTime('00010210T11:41:23')) (newdt,) = result
self.assertEqual(newdt, dt)
self.assertIs(type(newdt), xmlrpclib.DateTime)
self.assertIsNone(m)
def test_bug_1164912 (self): def test_bug_1164912 (self):
d = xmlrpclib.DateTime() d = xmlrpclib.DateTime()
...@@ -133,6 +162,25 @@ class XMLRPCTestCase(unittest.TestCase): ...@@ -133,6 +162,25 @@ class XMLRPCTestCase(unittest.TestCase):
xmlrpclib.loads(strg)[0][0]) xmlrpclib.loads(strg)[0][0])
self.assertRaises(TypeError, xmlrpclib.dumps, (arg1,)) self.assertRaises(TypeError, xmlrpclib.dumps, (arg1,))
def test_dump_bytes(self):
sample = b"my dog has fleas"
self.assertEqual(sample, xmlrpclib.Binary(sample))
for type_ in bytes, bytearray, xmlrpclib.Binary:
value = type_(sample)
s = xmlrpclib.dumps((value,))
result, m = xmlrpclib.loads(s, use_builtin_types=True)
(newvalue,) = result
self.assertEqual(newvalue, sample)
self.assertIs(type(newvalue), bytes)
self.assertIsNone(m)
result, m = xmlrpclib.loads(s, use_builtin_types=False)
(newvalue,) = result
self.assertEqual(newvalue, sample)
self.assertIs(type(newvalue), xmlrpclib.Binary)
self.assertIsNone(m)
def test_get_host_info(self): def test_get_host_info(self):
# see bug #3613, this raised a TypeError # see bug #3613, this raised a TypeError
transp = xmlrpc.client.Transport() transp = xmlrpc.client.Transport()
...@@ -140,9 +188,6 @@ class XMLRPCTestCase(unittest.TestCase): ...@@ -140,9 +188,6 @@ class XMLRPCTestCase(unittest.TestCase):
('host.tld', ('host.tld',
[('Authorization', 'Basic dXNlcg==')], {})) [('Authorization', 'Basic dXNlcg==')], {}))
def test_dump_bytes(self):
self.assertRaises(TypeError, xmlrpclib.dumps, (b"my dog has fleas",))
def test_ssl_presence(self): def test_ssl_presence(self):
try: try:
import ssl import ssl
......
...@@ -386,8 +386,8 @@ class Binary: ...@@ -386,8 +386,8 @@ class Binary:
if data is None: if data is None:
data = b"" data = b""
else: else:
if not isinstance(data, bytes): if not isinstance(data, (bytes, bytearray)):
raise TypeError("expected bytes, not %s" % raise TypeError("expected bytes or bytearray, not %s" %
data.__class__.__name__) data.__class__.__name__)
data = bytes(data) # Make a copy of the bytes! data = bytes(data) # Make a copy of the bytes!
self.data = data self.data = data
...@@ -559,6 +559,14 @@ class Marshaller: ...@@ -559,6 +559,14 @@ class Marshaller:
write("</string></value>\n") write("</string></value>\n")
dispatch[str] = dump_unicode dispatch[str] = dump_unicode
def dump_bytes(self, value, write):
write("<value><base64>\n")
encoded = base64.encodebytes(value)
write(encoded.decode('ascii'))
write("</base64></value>\n")
dispatch[bytes] = dump_bytes
dispatch[bytearray] = dump_bytes
def dump_array(self, value, write): def dump_array(self, value, write):
i = id(value) i = id(value)
if i in self.memo: if i in self.memo:
...@@ -629,7 +637,7 @@ class Unmarshaller: ...@@ -629,7 +637,7 @@ class Unmarshaller:
# and again, if you don't understand what's going on in here, # and again, if you don't understand what's going on in here,
# that's perfectly ok. # that's perfectly ok.
def __init__(self, use_datetime=False): def __init__(self, use_datetime=False, use_builtin_types=False):
self._type = None self._type = None
self._stack = [] self._stack = []
self._marks = [] self._marks = []
...@@ -637,7 +645,8 @@ class Unmarshaller: ...@@ -637,7 +645,8 @@ class Unmarshaller:
self._methodname = None self._methodname = None
self._encoding = "utf-8" self._encoding = "utf-8"
self.append = self._stack.append self.append = self._stack.append
self._use_datetime = use_datetime self._use_datetime = use_builtin_types or use_datetime
self._use_bytes = use_builtin_types
def close(self): def close(self):
# return response tuple and target method # return response tuple and target method
...@@ -749,6 +758,8 @@ class Unmarshaller: ...@@ -749,6 +758,8 @@ class Unmarshaller:
def end_base64(self, data): def end_base64(self, data):
value = Binary() value = Binary()
value.decode(data.encode("ascii")) value.decode(data.encode("ascii"))
if self._use_bytes:
value = value.data
self.append(value) self.append(value)
self._value = 0 self._value = 0
dispatch["base64"] = end_base64 dispatch["base64"] = end_base64
...@@ -860,21 +871,26 @@ FastMarshaller = FastParser = FastUnmarshaller = None ...@@ -860,21 +871,26 @@ FastMarshaller = FastParser = FastUnmarshaller = None
# #
# return A (parser, unmarshaller) tuple. # return A (parser, unmarshaller) tuple.
def getparser(use_datetime=False): def getparser(use_datetime=False, use_builtin_types=False):
"""getparser() -> parser, unmarshaller """getparser() -> parser, unmarshaller
Create an instance of the fastest available parser, and attach it Create an instance of the fastest available parser, and attach it
to an unmarshalling object. Return both objects. to an unmarshalling object. Return both objects.
""" """
if FastParser and FastUnmarshaller: if FastParser and FastUnmarshaller:
if use_datetime: if use_builtin_types:
mkdatetime = _datetime_type
mkbytes = base64.decodebytes
elif use_datetime:
mkdatetime = _datetime_type mkdatetime = _datetime_type
mkbytes = _binary
else: else:
mkdatetime = _datetime mkdatetime = _datetime
target = FastUnmarshaller(True, False, _binary, mkdatetime, Fault) mkbytes = _binary
target = FastUnmarshaller(True, False, mkbytes, mkdatetime, Fault)
parser = FastParser(target) parser = FastParser(target)
else: else:
target = Unmarshaller(use_datetime=use_datetime) target = Unmarshaller(use_datetime=use_datetime, use_builtin_types=use_builtin_types)
if FastParser: if FastParser:
parser = FastParser(target) parser = FastParser(target)
else: else:
...@@ -912,7 +928,7 @@ def dumps(params, methodname=None, methodresponse=None, encoding=None, ...@@ -912,7 +928,7 @@ def dumps(params, methodname=None, methodresponse=None, encoding=None,
encoding: the packet encoding (default is UTF-8) encoding: the packet encoding (default is UTF-8)
All 8-bit strings in the data structure are assumed to use the All byte strings in the data structure are assumed to use the
packet encoding. Unicode strings are automatically converted, packet encoding. Unicode strings are automatically converted,
where necessary. where necessary.
""" """
...@@ -971,7 +987,7 @@ def dumps(params, methodname=None, methodresponse=None, encoding=None, ...@@ -971,7 +987,7 @@ def dumps(params, methodname=None, methodresponse=None, encoding=None,
# (None if not present). # (None if not present).
# @see Fault # @see Fault
def loads(data, use_datetime=False): def loads(data, use_datetime=False, use_builtin_types=False):
"""data -> unmarshalled data, method name """data -> unmarshalled data, method name
Convert an XML-RPC packet to unmarshalled data plus a method Convert an XML-RPC packet to unmarshalled data plus a method
...@@ -980,7 +996,7 @@ def loads(data, use_datetime=False): ...@@ -980,7 +996,7 @@ def loads(data, use_datetime=False):
If the XML-RPC packet represents a fault condition, this function If the XML-RPC packet represents a fault condition, this function
raises a Fault exception. raises a Fault exception.
""" """
p, u = getparser(use_datetime=use_datetime) p, u = getparser(use_datetime=use_datetime, use_builtin_types=use_builtin_types)
p.feed(data) p.feed(data)
p.close() p.close()
return u.close(), u.getmethodname() return u.close(), u.getmethodname()
...@@ -1092,8 +1108,9 @@ class Transport: ...@@ -1092,8 +1108,9 @@ class Transport:
# that they can decode such a request # that they can decode such a request
encode_threshold = None #None = don't encode encode_threshold = None #None = don't encode
def __init__(self, use_datetime=False): def __init__(self, use_datetime=False, use_builtin_types=False):
self._use_datetime = use_datetime self._use_datetime = use_datetime
self._use_builtin_types = use_builtin_types
self._connection = (None, None) self._connection = (None, None)
self._extra_headers = [] self._extra_headers = []
...@@ -1154,7 +1171,8 @@ class Transport: ...@@ -1154,7 +1171,8 @@ class Transport:
def getparser(self): def getparser(self):
# get parser and unmarshaller # get parser and unmarshaller
return getparser(use_datetime=self._use_datetime) return getparser(use_datetime=self._use_datetime,
use_builtin_types=self._use_builtin_types)
## ##
# Get authorization info from host parameter # Get authorization info from host parameter
...@@ -1361,7 +1379,7 @@ class ServerProxy: ...@@ -1361,7 +1379,7 @@ class ServerProxy:
""" """
def __init__(self, uri, transport=None, encoding=None, verbose=False, def __init__(self, uri, transport=None, encoding=None, verbose=False,
allow_none=False, use_datetime=False): allow_none=False, use_datetime=False, use_builtin_types=False):
# establish a "logical" server connection # establish a "logical" server connection
# get the url # get the url
...@@ -1375,9 +1393,11 @@ class ServerProxy: ...@@ -1375,9 +1393,11 @@ class ServerProxy:
if transport is None: if transport is None:
if type == "https": if type == "https":
transport = SafeTransport(use_datetime=use_datetime) handler = SafeTransport
else: else:
transport = Transport(use_datetime=use_datetime) handler = Transport
transport = handler(use_datetime=use_datetime,
use_builtin_types=use_builtin_types)
self.__transport = transport self.__transport = transport
self.__encoding = encoding or 'utf-8' self.__encoding = encoding or 'utf-8'
......
...@@ -374,6 +374,8 @@ Core and Builtins ...@@ -374,6 +374,8 @@ Core and Builtins
Library Library
------- -------
- Issue #13297: Use bytes type to send and receive binary data through XMLRPC.
- Issue #6397: Support "/dev/poll" polling objects in select module, - Issue #6397: Support "/dev/poll" polling objects in select module,
under Solaris & derivatives. under Solaris & derivatives.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment