Python3 fixes for SoapAdapter serializer.

1. Remove the encode() helper method since it doesn't provide encoding in python3 at all.
2. In order to workaround the leading 'b' literal after encoding in python3,
   we delay the encoding step and will encode the whole soap message instead of individual values.
3. Added separate cases to handle Enum, integer and float to make the serializtion process more clear.
This commit is contained in:
Tianhao He 2016-03-04 15:56:01 -08:00
parent 2f0d539fd4
commit 7927002ff7
3 changed files with 93 additions and 42 deletions

@ -14,15 +14,20 @@
# limitations under the License.
from __future__ import absolute_import
import six
from six import PY2
from six import PY3
from six import reraise
from six.moves import http_client
from six.moves import StringIO
from six.moves import zip
from six import u
from six import iteritems
if PY3:
long = int
basestring = str
from six import u
import sys
import os
import socket
@ -33,11 +38,7 @@ from datetime import datetime
from xml.parsers.expat import ParserCreate
# We have our own escape functionality.
# from xml.sax.saxutils import escape
if PY2:
from cStringIO import StringIO
if PY3:
from io import StringIO
from pyVmomi.VmomiSupport import *
from pyVmomi.StubAdapterAccessorImpl import StubAdapterAccessorMixin
import pyVmomi.Iso8601
@ -99,11 +100,6 @@ MethodFault = GetVmodlType("vmodl.MethodFault")
## Localized MethodFault type
LocalizedMethodFault = GetVmodlType("vmodl.LocalizedMethodFault")
def encode(string, encoding):
if PY2:
return string.encode(encoding)
return u(string)
## Thumbprint mismatch exception
#
class ThumbprintMismatchException(Exception):
@ -136,15 +132,40 @@ def SetHandlers(obj, handlers):
obj.StartNamespaceDeclHandler,
obj.EndNamespaceDeclHandler) = handlers
## Serialize an object
## Serialize an object to bytes
#
# This function assumes CheckField(info, val) was already called
# @param val the value to serialize
# @param info the field
# @param version the version
# @param nsMap a dict of xml ns -> prefix
# @return the serialized object as a string
# @return the serialized object as bytes
# @param encoding Deprecated this is not used during serialization since we always
# use utf-8 to encode a request message. We didn't remove the
# parameter so it is still compatible with clients that are still using it.
def Serialize(val, info=None, version=None, nsMap=None, encoding=None):
return _SerializeToUnicode(val, info=info, version=version, nsMap=nsMap).encode(XML_ENCODING)
## Serialize an object to unicode
#
# This function assumes CheckField(info, val) was already called
# @param val the value to serialize
# @param info the field
# @param version the version
# @param nsMap a dict of xml ns -> prefix
# @return the serialized object as unicode
def SerializeToUnicode(val, info=None, version=None, nsMap=None):
return _SerializeToUnicode(val, info=info, version=version, nsMap=nsMap)
## Serialize an object to unicode
#
# This function assumes CheckField(info, val) was already called
# @param val the value to serialize
# @param info the field
# @param version the version
# @param nsMap a dict of xml ns -> prefix
# @return the serialized object as unicode
def _SerializeToUnicode(val, info=None, version=None, nsMap=None):
if version is None:
try:
if isinstance(val, list):
@ -162,7 +183,7 @@ def Serialize(val, info=None, version=None, nsMap=None, encoding=None):
info = Object(name="object", type=object, version=version, flags=0)
writer = StringIO()
SoapSerializer(writer, version, nsMap, encoding).Serialize(val, info)
SoapSerializer(writer, version, nsMap).Serialize(val, info)
return writer.getvalue()
## Serialize fault detail
@ -175,7 +196,7 @@ def Serialize(val, info=None, version=None, nsMap=None, encoding=None):
# @param info the field
# @param version the version
# @param nsMap a dict of xml ns -> prefix
# @return the serialized object as a string
# @return the serialized object as a unicode string
def SerializeFaultDetail(val, info=None, version=None, nsMap=None, encoding=None):
if version is None:
try:
@ -200,12 +221,14 @@ class SoapSerializer:
# @param writer File writer
# @param version the version
# @param nsMap a dict of xml ns -> prefix
def __init__(self, writer, version, nsMap, encoding):
# @param encoding Deprecated this is not used during serialization since we always
# use utf-8 to encode a request message. We didn't remove the
# parameter so it is still compatible with clients that are still using it.
def __init__(self, writer, version, nsMap, encoding=None):
""" Constructor """
self.writer = writer
self.version = version
self.nsMap = nsMap and nsMap or {}
self.encoding = encoding and encoding or XML_ENCODING
for ns, prefix in iteritems(self.nsMap):
if prefix == '':
self.defaultNS = ns
@ -271,7 +294,7 @@ class SoapSerializer:
attr = ' xmlns:{0}="{1}"'.format(prefix, ns)
return attr, prefix and prefix + ':' + name or name
## Serialize an object (internal)
## Serialize an object to unicode (internal)
#
# @param val the value to serialize
# @param info the field
@ -332,7 +355,7 @@ class SoapSerializer:
ns, name = GetQualifiedWsdlName(Type(val))
attr += ' type="{0}"'.format(name)
self.writer.write('<{0}{1}>{2}</{3}>'.format(info.name, attr,
encode(val._moId, self.encoding),
val._moId,
info.name))
elif isinstance(val, list):
if info.type is object:
@ -389,6 +412,13 @@ class SoapSerializer:
nsattr, qName = self._QName(Type(val), currDefNS)
attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
result = base64.b64encode(val)
if PY3:
# In python3 the bytes result after the base64 encoding has a
# leading 'b' which causes error when we use it to construct the
# soap message. Workaround the issue by converting the result to
# string. Since the result of base64 encoding contains only subset
# of ASCII chars, converting to string will not change the value.
result = str(result, XML_ENCODING)
self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr, result))
elif isinstance(val, bool):
if info.type is object:
@ -396,6 +426,17 @@ class SoapSerializer:
attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
result = val and "true" or "false"
self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr, result))
elif isinstance(val, six.integer_types) or isinstance(val, float):
if info.type is object:
nsattr, qName = self._QName(Type(val), currDefNS)
attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
result = six.text_type(val)
self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr, result))
elif isinstance(val, Enum):
if info.type is object:
nsattr, qName = self._QName(Type(val), currDefNS)
attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr, val))
else:
if info.type is object:
if isinstance(val, PropertyPath):
@ -404,24 +445,16 @@ class SoapSerializer:
nsattr, qName = self._QName(Type(val), currDefNS)
attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
# For some pyVmomi objects, isinstance(val, text_type) is True, but
# that object overrides certain string methods that are needed later.
# Convert val to a real string, regardless of its type.
val = str(val)
# Use UTF-8 rather than self.encoding. self.encoding is for
# output of serializer, while 'val' is our input. And regardless
# of what our output is, our input should be always UTF-8. Yes,
# it means that if you emit output in other encoding than UTF-8,
# you cannot serialize it again once more. That's feature, not
# a bug.
if PY2:
val = val.decode('UTF-8')
if isinstance(val, six.binary_type):
# Use UTF-8 rather than self.encoding. self.encoding is for
# output of serializer, while 'val' is our input. And regardless
# of what our output is, our input should be always UTF-8. Yes,
# it means that if you emit output in other encoding than UTF-8,
# you cannot serialize it again once more. That's feature, not
# a bug.
val = val.decode(XML_ENCODING)
result = XmlEscape(val)
self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr,
encode(result,
self.encoding)))
self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr, result))
## Serialize a a data object (internal)
#
@ -879,9 +912,9 @@ class SoapStubAdapterBase(StubAdapterBase):
result.append(SOAP_HEADER_START)
for key, val in iteritems(reqContexts):
# Note: Support req context of string type only
if not isinstance(val, basestring):
if not isinstance(val, six.string_types):
raise TypeError("Request context key ({0}) has non-string value ({1}) of {2}".format(key, val, type(val)))
ret = Serialize(val,
ret = _SerializeToUnicode(val,
Object(name=key, type=str, version=self.version),
self.version,
nsMap)
@ -896,15 +929,15 @@ class SoapStubAdapterBase(StubAdapterBase):
# Serialize soap body
result.extend([SOAP_BODY_START,
'<{0} xmlns="{1}">'.format(info.wsdlName, defaultNS),
Serialize(mo, Object(name="_this", type=ManagedObject,
_SerializeToUnicode(mo, Object(name="_this", type=ManagedObject,
version=self.version),
self.version, nsMap)])
# Serialize soap request parameters
for (param, arg) in zip(info.params, args):
result.append(Serialize(arg, param, self.version, nsMap))
result.append(_SerializeToUnicode(arg, param, self.version, nsMap))
result.extend(['</{0}>'.format(info.wsdlName), SOAP_BODY_END, SOAP_ENVELOPE_END])
return ''.join(result)
return ''.join(result).encode(XML_ENCODING)
## Subclass of HTTPConnection that connects over a Unix domain socket
## instead of a TCP port. The path of the socket is passed in place of
@ -1245,7 +1278,6 @@ class SoapStubAdapter(SoapStubAdapterBase):
# depend on the behavior that close() still leaves the socket semi-functional.
if sys.version_info[:2] < (2,7):
def _CloseConnection(self, conn):
# import pdb; pdb.set_trace()
if self.scheme == HTTPSConnectionWrapper and conn.sock:
conn.sock.shutdown(socket.SHUT_RDWR)
conn.close()

@ -21,6 +21,7 @@ from six import iteritems
from six import iterkeys
from six import itervalues
from six import text_type
from six import binary_type
from six import PY3
from datetime import datetime
from pyVmomi import Iso8601
@ -314,6 +315,13 @@ def FormatObject(val, info=Object(name="", type=object, flags=0), indent=0):
result = Iso8601.ISO8601Format(val)
elif isinstance(val, binary):
result = base64.b64encode(val)
if PY3:
# In python3 the bytes result after the base64 encoding has a
# leading 'b' which causes error when we use it to construct the
# soap message. Workaround the issue by converting the result to
# string. Since the result of base64 encoding contains only subset
# of ASCII chars, converting to string will not change the value.
result = str(result, 'utf-8')
else:
result = repr(val)
return start + result
@ -1290,7 +1298,7 @@ byte = type("byte", (int,), {})
short = type("short", (int,), {})
double = type("double", (float,), {})
URI = type("URI", (str,), {})
binary = type("binary", (str,), {})
binary = type("binary", (binary_type,), {})
PropertyPath = type("PropertyPath", (text_type,), {})
# _wsdlTypeMapNSs store namespaces added to _wsdlTypeMap in _SetWsdlType

@ -58,3 +58,14 @@ class SerializerTests(tests.VCRTestBase):
val = vim.vm.device.VirtualDeviceSpec.FileOperation()
# This line should not raise an exception, especially on Python 3.
SoapAdapter.Serialize(val)
def test_serialize_integer(self):
lp = vim.LongPolicy()
lp.inherited = False
lp.value = 100
SoapAdapter.Serialize(lp, version='vim.version.version10')
def test_serialize_float(self):
pc = vim.host.VsanInternalSystem.PolicyCost()
pc.diskSpaceToAddressSpaceRatio = 1.0
SoapAdapter.Serialize(pc, version='vim.version.version10')