diff --git a/www/py-webunit/files/patch-2to3 b/www/py-webunit/files/patch-2to3
new file mode 100644
index 000000000000..35fd5080f118
--- /dev/null
+++ b/www/py-webunit/files/patch-2to3
@@ -0,0 +1,548 @@
+--- webunit/cookie.py.orig 2010-05-05 05:21:19 UTC
++++ webunit/cookie.py
+@@ -1,4 +1,4 @@
+-import re, urlparse, Cookie
++import re, urllib.parse, http.cookies
+
+ class Error:
+ '''Handles a specific cookie error.
+@@ -33,7 +33,7 @@ def parse_cookie(text, qparmre=re.compile(
+ # We'll simply bail without raising an error
+ # if the cookie is invalid.
+ return result
+- if not result.has_key(name):
++ if name not in result:
+ result[name]=value
+ return result
+
+@@ -45,13 +45,12 @@ def decodeCookies(url, server, headers, cookies):
+ http://www.ietf.org/rfc/rfc2965.txt
+ '''
+ # the path of the request URL up to, but not including, the right-most /
+- request_path = urlparse.urlparse(url)[2]
++ request_path = urllib.parse.urlparse(url)[2]
+ if len(request_path) > 1 and request_path[-1] == '/':
+ request_path = request_path[:-1]
+
+- hdrcookies = Cookie.SimpleCookie("\n".join(map(lambda x: x.strip(),
+- headers.getallmatchingheaders('set-cookie'))))
+- for cookie in hdrcookies.values():
++ hdrcookies = http.cookies.SimpleCookie("\n".join([x.strip() for x in headers.getallmatchingheaders('set-cookie')]))
++ for cookie in list(hdrcookies.values()):
+ # XXX: there doesn't seem to be a way to determine if the
+ # cookie was set or defaulted to an empty string :(
+ if cookie['domain']:
+@@ -60,7 +59,7 @@ def decodeCookies(url, server, headers, cookies):
+ # reject if The value for the Domain attribute contains no
+ # embedded dots or does not start with a dot.
+ if '.' not in domain:
+- raise Error, 'Cookie domain "%s" has no "."'%domain
++ raise Error('Cookie domain "%s" has no "."'%domain)
+ if domain[0] != '.':
+ # per RFC2965 cookie domains with no leading '.' will have
+ # one added
+@@ -73,16 +72,16 @@ def decodeCookies(url, server, headers, cookies):
+ # but not:
+ # - someexample.com
+ if not server.endswith(domain) and domain[1:] != server:
+- raise Error, 'Cookie domain "%s" doesn\'t match '\
+- 'request host "%s"'%(domain, server)
++ raise Error('Cookie domain "%s" doesn\'t match '\
++ 'request host "%s"'%(domain, server))
+ # reject if the request-host is a FQDN (not IP address) and
+ # has the form HD, where D is the value of the Domain
+ # attribute, and H is a string that contains one or more dots.
+ if re.search(r'[a-zA-Z]', server):
+ H = server[:-len(domain)]
+ if '.' in H:
+- raise Error, 'Cookie domain "%s" too short '\
+- 'for request host "%s"'%(domain, server)
++ raise Error('Cookie domain "%s" too short '\
++ 'for request host "%s"'%(domain, server))
+ else:
+ domain = server
+
+@@ -92,8 +91,8 @@ def decodeCookies(url, server, headers, cookies):
+ # (noting that empty request path and '/' are often synonymous, yay)
+ if not (request_path.startswith(path) or (request_path == '' and
+ cookie['path'] == '/')):
+- raise Error, 'Cookie path "%s" doesn\'t match '\
+- 'request url "%s"'%(path, request_path)
++ raise Error('Cookie path "%s" doesn\'t match '\
++ 'request url "%s"'%(path, request_path))
+
+ bydom = cookies.setdefault(domain, {})
+ bypath = bydom.setdefault(path, {})
+--- webunit/HTMLParser.py.orig 2009-06-05 16:30:44 UTC
++++ webunit/HTMLParser.py
+@@ -183,7 +183,7 @@ class HTMLParser:
+ else:
+ if i < n-1:
+ raise HTMLParseError(
+- "invalid '<' construct: %s" % `rawdata[i:i+2]`,
++ "invalid '<' construct: %s" % repr(rawdata[i:i+2]),
+ self.getpos())
+ k = -1
+ if k < 0:
+@@ -274,7 +274,7 @@ class HTMLParser:
+ j = m.end()
+ else:
+ raise HTMLParseError(
+- "unexpected char in declaration: %s" % `rawdata[j]`,
++ "unexpected char in declaration: %s" % repr(rawdata[j]),
+ self.getpos())
+ return -1 # incomplete
+
+@@ -330,7 +330,7 @@ class HTMLParser:
+ else:
+ offset = offset + len(self.__starttag_text)
+ raise HTMLParseError("junk characters in start tag: %s"
+- % `rawdata[k:endpos][:20]`,
++ % repr(rawdata[k:endpos][:20]),
+ (lineno, offset))
+ if end[-2:] == '/>':
+ # XHTML-style empty tag:
+@@ -384,7 +384,7 @@ class HTMLParser:
+ j = match.end()
+ match = endtagfind.match(rawdata, i) # + tag + >
+ if not match:
+- raise HTMLParseError("bad end tag: %s" % `rawdata[i:j]`,
++ raise HTMLParseError("bad end tag: %s" % repr(rawdata[i:j]),
+ self.getpos())
+ tag = match.group(1)
+ self.handle_endtag(string.lower(tag))
+--- webunit/SimpleDOM.py.orig 2009-06-05 16:30:44 UTC
++++ webunit/SimpleDOM.py
+@@ -35,8 +35,8 @@ Simple usage:
+ import sys, string
+
+ # NOTE this is using a modified HTMLParser
+-from HTMLParser import HTMLParser, HTMLParseError
+-from utility import Upload
++from .HTMLParser import HTMLParser, HTMLParseError
++from .utility import Upload
+
+ BOOLEAN_HTML_ATTRS = [
+ # List of Boolean attributes in HTML that may be given in
+@@ -139,7 +139,7 @@ class SimpleDOMNode:
+ for entry in l:
+ if hasattr(entry, 'id') and entry.id == id:
+ return entry
+- raise ValueError, 'No %r with id %r'%(name, id)
++ raise ValueError('No %r with id %r'%(name, id))
+
+ def getByNameFlat(self, name):
+ '''Return all nodes of type "name" from the contents of this node.
+@@ -182,21 +182,21 @@ class SimpleDOMNode:
+ return self.getContents()[item]
+
+ def hasattr(self, attr):
+- return self.__dict__['__attributes'].has_key(attr)
++ return attr in self.__dict__['__attributes']
+
+ def getattr(self, attr, default=_marker):
+- if self.__dict__['__attributes'].has_key(attr):
++ if attr in self.__dict__['__attributes']:
+ return self.__dict__['__attributes'][attr]
+ if default is _marker:
+- raise AttributeError, attr
++ raise AttributeError(attr)
+ return default
+
+ def __getattr__(self, attr):
+- if self.__dict__['__attributes'].has_key(attr):
++ if attr in self.__dict__['__attributes']:
+ return self.__dict__['__attributes'][attr]
+- if self.__dict__.has_key(attr):
++ if attr in self.__dict__:
+ return self.__dict__[attr]
+- raise AttributeError, attr
++ raise AttributeError(attr)
+
+ def __len__(self):
+ return len(self.getContents())
+@@ -209,7 +209,7 @@ class SimpleDOMNode:
+
+ def __str__(self):
+ attrs = []
+- for attr in self.__dict__['__attributes'].items():
++ for attr in list(self.__dict__['__attributes'].items()):
+ if attr[0] in BOOLEAN_HTML_ATTRS:
+ attrs.append(attr[0])
+ else:
+@@ -339,8 +339,8 @@ class SimpleDOMParser(HTMLParser):
+
+ def handle_starttag(self, tag, attrs):
+ if self.__debug:
+- print '\n>handle_starttag', tag
+- print self.tagstack
++ print('\n>handle_starttag', tag)
++ print(self.tagstack)
+ self.close_para_tags(tag)
+ self.tagstack.append(tag)
+ d = {}
+@@ -352,8 +352,8 @@ class SimpleDOMParser(HTMLParser):
+
+ def handle_startendtag(self, tag, attrs):
+ if self.__debug:
+- print '> etc. in the source is an error
+ raise EmptyTagError(tag, self.getpos())
+@@ -375,7 +375,7 @@ class SimpleDOMParser(HTMLParser):
+ if tag in EMPTY_HTML_TAGS:
+ return
+ close_to = -1
+- if BLOCK_CLOSING_TAG_MAP.has_key(tag):
++ if tag in BLOCK_CLOSING_TAG_MAP:
+ blocks_to_close = BLOCK_CLOSING_TAG_MAP[tag]
+ for i in range(len(self.tagstack)):
+ t = self.tagstack[i]
+@@ -404,8 +404,8 @@ class SimpleDOMParser(HTMLParser):
+
+ def implied_endtag(self, tag, implied):
+ if self.__debug:
+- print '