Commit cd906440 authored by Martijn Pieters's avatar Martijn Pieters

Update xmlrpclib.py to the Python2.2 version, while preserving the Zope

specific addition of marshalling None to <boolean>0</boolean>.

Benefits gained:

  - Uses Expat when available (like in Zope), which makes unmarshalling
    (receiving XML-RPC calls) much,much faster.

  - New library imports other python modules lazily, which helps speed up
    Zope startup time.

  - Faster escape method for outgoing calls.

  - Fixes for doubles and larger integers.

  - Allow XML-RPC calls without an empty <params /> (fixes Zope Collector
    #465)

Additional fix:

  - Fix SlowParser to handle CDATA sections as well. With the ExpatParser
    this is less of a problem (fixes Zope Collector #547). Also see Python
    SF issue #601534.
parent 73456dc0
......@@ -8,16 +8,10 @@
# implement XML-RPC servers.
#
# Notes:
# this version uses the sgmlop XML parser, if installed. this is
# typically 10-15x faster than using Python's standard XML parser.
#
# you can get the sgmlop distribution from:
#
# http://www.pythonware.com/products/xml/sgmlop.htm
#
# this version is designed to work with Python 1.5.2 or newer.
# unicode encoding support requires at least Python 1.6.
# experimental HTTPS requires Python 2.0 built with SSL sockets.
# expat parser support requires Python 2.0 with pyexpat support.
#
# History:
# 1999-01-14 fl Created
......@@ -30,12 +24,23 @@
# 1999-06-20 fl Speed improvements, pluggable parsers/transports (0.9.8)
# 2000-11-28 fl Changed boolean to check the truth value of its argument
# 2001-02-24 fl Added encoding/Unicode/SafeTransport patches
# 2001-02-26 fl Added compare support to wrappers (0.9.9)
# 2001-02-26 fl Added compare support to wrappers (0.9.9/1.0b1)
# 2001-03-28 fl Make sure response tuple is a singleton
# 2001-03-29 fl Don't require empty params element (from Nicholas Riley)
# 2001-06-10 fl Folded in _xmlrpclib accelerator support (1.0b2)
# 2001-08-20 fl Base xmlrpclib.Error on built-in Exception (from Paul Prescod)
# 2001-09-03 fl Allow Transport subclass to override getparser
# 2001-09-10 fl Lazy import of urllib, cgi, xmllib (20x import speedup)
# 2001-10-01 fl Remove containers from memo cache when done with them
# 2001-10-01 fl Use faster escape method (80% dumps speedup)
# 2001-10-10 sm Allow long ints to be passed as ints if they don't overflow
# 2001-10-17 sm test for int and long overflow (allows use on 64-bit systems)
# 2001-11-12 fl Use repr() to marshal doubles (from Paul Felix)
#
# Copyright (c) 1999-2001 by Secret Labs AB.
# Copyright (c) 1999-2001 by Fredrik Lundh.
#
# fredrik@pythonware.com
# info@pythonware.com
# http://www.pythonware.com
#
# --------------------------------------------------------------------
......@@ -68,34 +73,62 @@
# --------------------------------------------------------------------
#
# things to fix before 1.0 final:
# TODO: unicode marshalling -DONE
# TODO: ascii-compatible encoding support -DONE
# TODO: safe transport -DONE (but mostly untested)
# TODO: sgmlop memory leak -DONE
# TODO: sgmlop xml parsing -DONE
# TODO: support unicode method names -DONE
# TODO: update selftest -DONE
# TODO: add docstrings -DONE
# TODO: clean up parser encoding (trust the parser) -DONE
# things to look into:
# TODO: support basic authentication (see robin's patch)
# TODO: fix host tuple handling in the server constructor
# TODO: let transport verify schemes
# TODO: update documentation
# TODO: authentication plugins
# TODO: memo problem (see HP's mail)
"""
An XML-RPC client interface for Python.
The marshalling and response parser code can also be used to
implement XML-RPC servers.
Exported exceptions:
Error Base class for client errors
ProtocolError Indicates an HTTP protocol error
ResponseError Indicates a broken response package
Fault Indicates an XML-RPC fault package
Exported classes:
ServerProxy Represents a logical connection to an XML-RPC server
Boolean boolean wrapper to generate a "boolean" XML-RPC value
DateTime dateTime wrapper for an ISO 8601 string or time tuple or
localtime integer value to generate a "dateTime.iso8601"
XML-RPC value
Binary binary data wrapper
SlowParser Slow but safe standard parser (based on xmllib)
Marshaller Generate an XML-RPC params chunk from a Python data structure
Unmarshaller Unmarshal an XML-RPC response from incoming XML event message
Transport Handles an HTTP transaction to an XML-RPC server
SafeTransport Handles an HTTPS transaction to an XML-RPC server
Exported constants:
True
False
Exported functions:
boolean Convert any Python value to an XML-RPC boolean
getparser Create instance of the fastest available parser & attach
to an unmarshalling object
dumps Convert an argument tuple or a Fault instance to an XML-RPC
request (or response, if the methodresponse option is used).
loads Convert an XML-RPC packet to unmarshalled data plus a method
name (None if not present).
"""
import re, string, time, operator
import urllib, xmllib
from types import *
from cgi import escape
try:
import sgmlop
if not hasattr(sgmlop, "XMLParser"):
raise ImportError
except ImportError:
sgmlop = None # accelerator not available
from types import *
try:
unicode
......@@ -104,22 +137,43 @@ except NameError:
def _decode(data, encoding, is8bit=re.compile("[\x80-\xff]").search):
# decode non-ascii string (if possible)
if unicode and is8bit(data):
if unicode and encoding and is8bit(data):
data = unicode(data, encoding)
return data
__version__ = "0.9.9"
def escape(s, replace=string.replace):
s = replace(s, "&", "&amp;")
s = replace(s, "<", "&lt;")
return replace(s, ">", "&gt;",)
MAXINT = 2L**31-1
MININT = -2L**31
if unicode:
def _stringify(string):
# convert to 7-bit ascii if possible
try:
return str(string)
except UnicodeError:
return string
else:
def _stringify(string):
return string
__version__ = "1.0.0"
# --------------------------------------------------------------------
# Exceptions
class Error:
# base class for client errors
pass
class Error(Exception):
"""Base class for client errors."""
def __str__(self):
return repr(self)
class ProtocolError(Error):
# indicates an HTTP protocol error
"""Indicates an HTTP protocol error."""
def __init__(self, url, errcode, errmsg, headers):
Error.__init__(self)
self.url = url
self.errcode = errcode
self.errmsg = errmsg
......@@ -131,12 +185,13 @@ class ProtocolError(Error):
)
class ResponseError(Error):
# indicates a broken response package
"""Indicates a broken response package."""
pass
class Fault(Error):
# indicates a XML-RPC fault package
"""Indicates an XML-RPC fault package."""
def __init__(self, faultCode, faultString, **extra):
Error.__init__(self)
self.faultCode = faultCode
self.faultString = faultString
def __repr__(self):
......@@ -145,14 +200,14 @@ class Fault(Error):
(self.faultCode, repr(self.faultString))
)
# --------------------------------------------------------------------
# Special values
# boolean wrapper
# use True or False to generate a "boolean" XML-RPC value
class Boolean:
"""Boolean-value wrapper.
Use True or False to generate a "boolean" XML-RPC value.
"""
def __init__(self, value = 0):
self.value = operator.truth(value)
......@@ -180,15 +235,14 @@ class Boolean:
True, False = Boolean(1), Boolean(0)
def boolean(value, truefalse=(False, True)):
# convert any Python value to XML-RPC boolean
"""Convert any Python value to XML-RPC 'boolean'."""
return truefalse[operator.truth(value)]
#
# dateTime wrapper
# wrap your iso8601 string or time tuple or localtime integer value
# in this class to generate a "dateTime.iso8601" XML-RPC value
class DateTime:
"""DateTime wrapper for an ISO 8601 string or time tuple or
localtime integer value to generate 'dateTime.iso8601' XML-RPC
value.
"""
def __init__(self, value=0):
if not isinstance(value, StringType):
......@@ -215,10 +269,13 @@ class DateTime:
out.write(self.value)
out.write("</dateTime.iso8601></value>\n")
#
# binary data wrapper
def datetime(data):
value = DateTime()
value.decode(data)
return value
class Binary:
"""Wrapper for binary data."""
def __init__(self, data=None):
self.data = data
......@@ -238,18 +295,40 @@ class Binary:
base64.encode(StringIO.StringIO(self.data), out)
out.write("</base64></value>\n")
WRAPPERS = DateTime, Binary, Boolean
def binary(data):
value = Binary()
value.decode(data)
return value
WRAPPERS = DateTime, Binary, Boolean
# --------------------------------------------------------------------
# XML parsers
if sgmlop:
try:
# optional xmlrpclib accelerator. for more information on this
# component, contact info@pythonware.com
import _xmlrpclib
FastParser = _xmlrpclib.Parser
FastUnmarshaller = _xmlrpclib.Unmarshaller
except (AttributeError, ImportError):
FastParser = FastUnmarshaller = None
class FastParser:
# sgmlop based XML parser. this is typically 15x faster
# than SlowParser...
#
# the SGMLOP parser is about 15x faster than Python's builtin
# XML parser. SGMLOP sources can be downloaded from:
#
# http://www.pythonware.com/products/xml/sgmlop.htm
#
try:
import sgmlop
if not hasattr(sgmlop, "XMLParser"):
raise ImportError
except ImportError:
SgmlopParser = None # sgmlop accelerator not available
else:
class SgmlopParser:
def __init__(self, target):
# setup callbacks
......@@ -274,6 +353,7 @@ if sgmlop:
self.parser = self.feed = None # nuke circular reference
def handle_proc(self, tag, attr):
import re
m = re.search("encoding\s*=\s*['\"]([^\"']+)[\"']", attr)
if m:
self.handle_xml(m.group(1), 1)
......@@ -285,33 +365,64 @@ if sgmlop:
except KeyError:
self.handle_data("&%s;" % entity)
try:
from xml.parsers import expat
if not hasattr(expat, "ParserCreate"):
raise ImportError, "ParserCreate"
except ImportError:
ExpatParser = None
else:
class ExpatParser:
# fast expat parser for Python 2.0. this is about 50%
# slower than sgmlop, on roundtrip testing
def __init__(self, target):
self._parser = parser = expat.ParserCreate(None, None)
self._target = target
parser.StartElementHandler = target.start
parser.EndElementHandler = target.end
parser.CharacterDataHandler = target.data
encoding = None
if not parser.returns_unicode:
encoding = "utf-8"
target.xml(encoding, None)
def feed(self, data):
self._parser.Parse(data, 0)
FastParser = None
class SlowParser(xmllib.XMLParser):
# slow but safe standard parser, based on the XML parser in
# Python's standard library
def close(self):
self._parser.Parse("", 1) # end of data
del self._target, self._parser # get rid of circular references
class SlowParser:
"""Default XML parser (based on xmllib.XMLParser)."""
# this is about 10 times slower than sgmlop, on roundtrip
# testing.
def __init__(self, target):
import xmllib # lazy subclassing (!)
if xmllib.XMLParser not in SlowParser.__bases__:
SlowParser.__bases__ = (xmllib.XMLParser,)
self.handle_xml = target.xml
self.unknown_starttag = target.start
self.handle_data = target.data
self.handle_cdata = target.data
self.unknown_endtag = target.end
xmllib.XMLParser.__init__(self)
try:
xmllib.XMLParser.__init__(self, accept_utf8=1)
except TypeError:
xmllib.XMLParser.__init__(self) # pre-2.0
# --------------------------------------------------------------------
# XML-RPC marshalling and unmarshalling code
class Marshaller:
"""Generate an XML-RPC params chunk from a Python data structure"""
"""Generate an XML-RPC params chunk from a Python data structure.
# USAGE: create a marshaller instance for each set of parameters,
# and use "dumps" to convert your data (represented as a tuple) to
# a XML-RPC params chunk. to write a fault response, pass a Fault
# instance instead. you may prefer to use the "dumps" convenience
# function for this purpose (see below).
Create a Marshaller instance for each set of parameters, and use
the "dumps" method to convert your data (represented as a tuple)
to an XML-RPC params chunk. To write a fault response, pass a
Fault instance instead. You may prefer to use the "dumps" module
function for this purpose.
"""
# by the way, if you don't understand what's going on in here,
# that's perfectly ok.
......@@ -333,6 +444,11 @@ class Marshaller:
write("</fault>\n")
else:
# parameter block
# FIXME: the xml-rpc specification allows us to leave out
# the entire <params> block if there are no parameters.
# however, changing this may break older code (including
# old versions of xmlrpclib.py), so this is better left as
# is for now. See @XMLRPC3 for more information. /F
write("<params>\n")
for v in values:
write("<param>\n")
......@@ -352,19 +468,29 @@ class Marshaller:
f(self, value)
def dump_int(self, value):
# in case ints are > 32 bits
if value > MAXINT or value < MININT:
raise OverflowError, "int exceeds XML-RPC limits"
self.write("<value><int>%s</int></value>\n" % value)
dispatch[IntType] = dump_int
def dump_long(self, value):
# in case ints are > 32 bits
if value > MAXINT or value < MININT:
raise OverflowError, "long int exceeds XML-RPC limits"
self.write("<value><int>%s</int></value>\n" % int(value))
dispatch[LongType] = dump_long
def dump_double(self, value):
self.write("<value><double>%s</double></value>\n" % value)
self.write("<value><double>%s</double></value>\n" % repr(value))
dispatch[FloatType] = dump_double
def dump_string(self, value):
def dump_string(self, value, escape=escape):
self.write("<value><string>%s</string></value>\n" % escape(value))
dispatch[StringType] = dump_string
if unicode:
def dump_unicode(self, value):
def dump_unicode(self, value, escape=escape):
value = value.encode(self.encoding)
self.write("<value><string>%s</string></value>\n" % escape(value))
dispatch[UnicodeType] = dump_unicode
......@@ -376,35 +502,43 @@ class Marshaller:
self.write("<value><boolean>0</boolean></value>\n")
dispatch[NoneType] = dump_none
def container(self, value):
def opencontainer(self, value):
if value:
i = id(value)
if self.memo.has_key(i):
raise TypeError, "cannot marshal recursive data structures"
self.memo[i] = None
def closecontainer(self, value):
if value:
del self.memo[id(value)]
def dump_array(self, value):
self.container(value)
self.opencontainer(value)
write = self.write
dump = self.__dump
write("<value><array><data>\n")
for v in value:
self.__dump(v)
dump(v)
write("</data></array></value>\n")
self.closecontainer(value)
dispatch[TupleType] = dump_array
dispatch[ListType] = dump_array
def dump_struct(self, value):
self.container(value)
def dump_struct(self, value, escape=escape):
self.opencontainer(value)
write = self.write
dump = self.__dump
write("<value><struct>\n")
for k, v in value.items():
write("<member>\n")
if type(k) is not StringType:
raise TypeError, "dictionary key must be string"
write("<name>%s</name>\n" % escape(k))
self.__dump(v)
dump(v)
write("</member>\n")
write("</struct></value>\n")
self.closecontainer(value)
dispatch[DictType] = dump_struct
def dump_instance(self, value):
......@@ -417,13 +551,13 @@ class Marshaller:
dispatch[InstanceType] = dump_instance
class Unmarshaller:
"""Unmarshal an XML-RPC response, based on incoming XML event
messages (start, data, end). Call close() to get the resulting
data structure.
# unmarshal an XML-RPC response, based on incoming XML event
# messages (start, data, end). call close to get the resulting
# data structure
# note that this reader is fairly tolerant, and gladly accepts
# bogus XML-RPC data without complaining (but not bogus XML).
Note that this reader is fairly tolerant, and gladly accepts bogus
XML-RPC data without complaining (but not bogus XML).
"""
# and again, if you don't understand what's going on in here,
# that's perfectly ok.
......@@ -452,12 +586,12 @@ class Unmarshaller:
# event handlers
def xml(self, encoding, standalone):
self._encoding = encoding or "utf-8"
self._encoding = encoding
# FIXME: assert standalone == 1 ???
def start(self, tag, attrs):
# prepare to handle this element
if tag in ("array", "struct"):
if tag == "array" or tag == "struct":
self._marks.append(len(self._stack))
self._data = []
self._value = (tag == "value")
......@@ -465,52 +599,62 @@ class Unmarshaller:
def data(self, text):
self._data.append(text)
dispatch = {}
def end(self, tag):
def end(self, tag, join=string.join):
# call the appropriate end tag handler
try:
f = self.dispatch[tag]
except KeyError:
pass # unknown tag ?
else:
return f(self)
return f(self, join(self._data, ""))
#
# accelerator support
def end_dispatch(self, tag, data):
# dispatch data
try:
f = self.dispatch[tag]
except KeyError:
pass # unknown tag ?
else:
return f(self, data)
#
# element decoders
def end_boolean(self, join=string.join):
value = join(self._data, "")
if value == "0":
dispatch = {}
def end_boolean(self, data):
if data == "0":
self.append(False)
elif value == "1":
elif data == "1":
self.append(True)
else:
raise TypeError, "bad boolean value"
self._value = 0
dispatch["boolean"] = end_boolean
def end_int(self, join=string.join):
self.append(int(join(self._data, "")))
def end_int(self, data):
self.append(int(data))
self._value = 0
dispatch["i4"] = end_int
dispatch["int"] = end_int
def end_double(self, join=string.join):
self.append(float(join(self._data, "")))
def end_double(self, data):
self.append(float(data))
self._value = 0
dispatch["double"] = end_double
def end_string(self, join=string.join):
data = join(self._data, "")
def end_string(self, data):
if self._encoding:
data = _decode(data, self._encoding)
self.append(data)
self.append(_stringify(data))
self._value = 0
dispatch["string"] = end_string
dispatch["name"] = end_string # struct keys are always strings
def end_array(self):
def end_array(self, data):
mark = self._marks[-1]
del self._marks[-1]
# map arrays to Python lists
......@@ -518,51 +662,51 @@ class Unmarshaller:
self._value = 0
dispatch["array"] = end_array
def end_struct(self):
def end_struct(self, data):
mark = self._marks[-1]
del self._marks[-1]
# map structs to Python dictionaries
dict = {}
items = self._stack[mark:]
for i in range(0, len(items), 2):
dict[items[i]] = items[i+1]
dict[_stringify(items[i])] = items[i+1]
self._stack[mark:] = [dict]
self._value = 0
dispatch["struct"] = end_struct
def end_base64(self, join=string.join):
def end_base64(self, data):
value = Binary()
value.decode(join(self._data, ""))
value.decode(data)
self.append(value)
self._value = 0
dispatch["base64"] = end_base64
def end_dateTime(self, join=string.join):
def end_dateTime(self, data):
value = DateTime()
value.decode(join(self._data, ""))
value.decode(data)
self.append(value)
dispatch["dateTime.iso8601"] = end_dateTime
def end_value(self):
def end_value(self, data):
# if we stumble upon an value element with no internal
# elements, treat it as a string element
if self._value:
self.end_string()
self.end_string(data)
dispatch["value"] = end_value
def end_params(self):
def end_params(self, data):
self._type = "params"
dispatch["params"] = end_params
def end_fault(self):
def end_fault(self, data):
self._type = "fault"
dispatch["fault"] = end_fault
def end_methodName(self, join=string.join):
data = join(self._data, "")
def end_methodName(self, data):
if self._encoding:
data = _decode(data, self._encoding)
self._methodname = data
self._type = "methodName" # no params
dispatch["methodName"] = end_methodName
......@@ -572,35 +716,54 @@ class Unmarshaller:
def getparser():
"""getparser() -> parser, unmarshaller
Create an instance of the fastest available parser, and attach
it to an unmarshalling object. Return both objects.
Create an instance of the fastest available parser, and attach it
to an unmarshalling object. Return both objects.
"""
target = Unmarshaller()
if FastParser:
return FastParser(target), target
return SlowParser(target), target
if FastParser and FastUnmarshaller:
target = FastUnmarshaller(True, False, binary, datetime)
parser = FastParser(target)
else:
target = Unmarshaller()
if FastParser:
parser = FastParser(target)
elif SgmlopParser:
parser = SgmlopParser(target)
elif ExpatParser:
parser = ExpatParser(target)
else:
parser = SlowParser(target)
return parser, target
def dumps(params, methodname=None, methodresponse=None, encoding=None):
"""data [,options] -> marshalled data
Convert a tuple or a fault object to an XML-RPC request (or
response, if the methodsresponse option is used).
Convert an argument tuple or a Fault instance to an XML-RPC
request (or response, if the methodresponse option is used).
In addition to the data object, the following options can be
given as keyword arguments:
In addition to the data object, the following options can be given
as keyword arguments:
methodname: the method name for a methodCall packet
methodresponse: true to create a methodResponse packet
methodresponse: true to create a methodResponse packet.
If this option is used with a tuple, the tuple must be
a singleton (i.e. it can contain only one element).
encoding: the packet encoding (default is UTF-8)
All 8-bit strings in the data structure are assumed to use the
packet encoding. Unicode strings are automatically converted,
as necessary.
where necessary.
"""
assert type(params) == TupleType or isinstance(params, Fault),\
assert isinstance(params, TupleType) or isinstance(params, Fault),\
"argument must be tuple or Fault instance"
if isinstance(params, Fault):
methodresponse = 1
elif methodresponse and isinstance(params, TupleType):
assert len(params) == 1, "response tuple must be a singleton"
if not encoding:
encoding = "utf-8"
......@@ -624,8 +787,8 @@ def dumps(params, methodname=None, methodresponse=None, encoding=None):
data,
"</methodCall>\n"
)
elif methodresponse or isinstance(params, Fault):
# a method response
elif methodresponse:
# a method response, or a fault structure
data = (
xmlheader,
"<methodResponse>\n",
......@@ -667,7 +830,7 @@ class _Method:
class Transport:
"""Handles an HTTP transaction to an XML-RPC server"""
"""Handles an HTTP transaction to an XML-RPC server."""
# client identifier (may be overridden)
user_agent = "xmlrpclib.py/%s (by www.pythonware.com)" % __version__
......@@ -697,6 +860,10 @@ class Transport:
return self.parse_response(h.getfile())
def getparser(self):
# get parser and unmarshaller
return getparser()
def make_connection(self, host):
# create a HTTP connection object from a host descriptor
import httplib
......@@ -721,7 +888,7 @@ class Transport:
def parse_response(self, f):
# read response from input file, and parse it
p, u = getparser()
p, u = self.getparser()
while 1:
response = f.read(1024)
......@@ -737,7 +904,7 @@ class Transport:
return u.close()
class SafeTransport(Transport):
"""Handles an HTTPS transaction to an XML-RPC server"""
"""Handles an HTTPS transaction to an XML-RPC server."""
def make_connection(self, host):
# create a HTTPS connection object from a host descriptor
......@@ -760,7 +927,7 @@ class SafeTransport(Transport):
host, x509 = host
connection.putheader("Host", host)
class Server:
class ServerProxy:
"""uri [,options] -> a logical connection to an XML-RPC server
uri is the connection point on the server, given as
......@@ -786,6 +953,7 @@ class Server:
# establish a "logical" server connection
# get the url
import urllib
type, uri = urllib.splittype(uri)
if type not in ("http", "https"):
raise IOError, "unsupported XML-RPC protocol"
......@@ -822,7 +990,7 @@ class Server:
def __repr__(self):
return (
"<Server proxy for %s%s>" %
"<ServerProxy for %s%s>" %
(self.__host, self.__handler)
)
......@@ -835,6 +1003,9 @@ class Server:
# note: to call a remote object with an non-standard name, use
# result getattr(server, "strange-python-name")(args)
# compatibility
Server = ServerProxy
# --------------------------------------------------------------------
# test code
......@@ -842,8 +1013,8 @@ if __name__ == "__main__":
# simple test program (from the XML-RPC specification)
# server = Server("http://localhost:8000") # local server
server = Server("http://betty.userland.com")
# server = ServerProxy("http://localhost:8000") # local server
server = ServerProxy("http://betty.userland.com")
print server
......
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