[Zope3-checkins] SVN: Zope3/trunk/src/ClientForm/ClientForm.py Update ClientForm to version 0.2.9.

Marius Gedminas marius at pov.lt
Mon Jul 21 11:58:42 EDT 2008


Log message for revision 88652:
  Update ClientForm to version 0.2.9.
  
  Fixes https://bugs.launchpad.net/zope3/+bug/243785.
  
  

Changed:
  U   Zope3/trunk/src/ClientForm/ClientForm.py

-=-
Modified: Zope3/trunk/src/ClientForm/ClientForm.py
===================================================================
--- Zope3/trunk/src/ClientForm/ClientForm.py	2008-07-21 15:54:05 UTC (rev 88651)
+++ Zope3/trunk/src/ClientForm/ClientForm.py	2008-07-21 15:58:42 UTC (rev 88652)
@@ -15,24 +15,20 @@
 HTML 4.01 Specification, W3C Recommendation 24 December 1999
 
 
-Copyright 2002-2006 John J. Lee <jjl at pobox.com>
+Copyright 2002-2007 John J. Lee <jjl at pobox.com>
 Copyright 2005 Gary Poster
 Copyright 2005 Zope Corporation
 Copyright 1998-2000 Gisle Aas.
 
 This code is free software; you can redistribute it and/or modify it
-under the terms of the BSD License (see the file COPYING included with
-the distribution).
+under the terms of the BSD or ZPL 2.1 licenses (see the file
+COPYING.txt included with the distribution).
 
 """
 
 # XXX
-# Remove unescape_attr method
 # Remove parser testing hack
 # safeUrl()-ize action
-# Really should to merge CC, CF, pp and mechanize as soon as mechanize
-#  goes to beta...
-# Add url attribute to ParseError
 # Switch to unicode throughout (would be 0.3.x)
 #  See Wichert Akkerman's 2004-01-22 message to c.l.py.
 # Add charset parameter to Content-type headers?  How to find value??
@@ -41,11 +37,6 @@
 #  Does file upload work when name is missing?  Sourceforge tracker form
 #   doesn't like it.  Check standards, and test with Apache.  Test
 #   binary upload with Apache.
-# Controls can have name=None (e.g. forms constructed partly with
-#  JavaScript), but find_control can't be told to find a control
-#  with that name, because None there means 'unspecified'.  Can still
-#  get at by nr, but would be nice to be able to specify something
-#  equivalent to name=None, too.
 # mailto submission & enctype text/plain
 # I'm not going to fix this unless somebody tells me what real servers
 #  that want this encoding actually expect: If enctype is
@@ -65,6 +56,15 @@
 # Work on DOMForm.
 # XForms?  Don't know if there's a need here.
 
+__all__ = ['AmbiguityError', 'CheckboxControl', 'Control',
+           'ControlNotFoundError', 'FileControl', 'FormParser', 'HTMLForm',
+           'HiddenControl', 'IgnoreControl', 'ImageControl', 'IsindexControl',
+           'Item', 'ItemCountError', 'ItemNotFoundError', 'Label',
+           'ListControl', 'LocateError', 'Missing', 'ParseError', 'ParseFile',
+           'ParseFileEx', 'ParseResponse', 'ParseResponseEx','PasswordControl',
+           'RadioControl', 'ScalarControl', 'SelectControl',
+           'SubmitButtonControl', 'SubmitControl', 'TextControl',
+           'TextareaControl', 'XHTMLCompatibleFormParser']
 
 try: True
 except NameError:
@@ -79,6 +79,7 @@
 
 try:
     import logging
+    import inspect
 except ImportError:
     def debug(msg, *args, **kwds):
         pass
@@ -90,11 +91,7 @@
         if OPTIMIZATION_HACK:
             return
 
-        try:
-            raise Exception()
-        except:
-            caller_name = (
-                sys.exc_info()[2].tb_frame.f_back.f_back.f_code.co_name)
+        caller_name = inspect.stack()[1][3]
         extended_msg = '%%s %s' % msg
         extended_args = (caller_name,)+args
         debug = _logger.debug(extended_msg, *extended_args, **kwds)
@@ -109,27 +106,45 @@
 
 import sys, urllib, urllib2, types, mimetools, copy, urlparse, \
        htmlentitydefs, re, random
-from urlparse import urljoin
 from cStringIO import StringIO
 
+import sgmllib
+# monkeypatch to fix http://www.python.org/sf/803422 :-(
+sgmllib.charref = re.compile("&#(x?[0-9a-fA-F]+)[^0-9a-fA-F]")
+
+# HTMLParser.HTMLParser is recent, so live without it if it's not available
+# (also, sgmllib.SGMLParser is much more tolerant of bad HTML)
 try:
+    import HTMLParser
+except ImportError:
+    HAVE_MODULE_HTMLPARSER = False
+else:
+    HAVE_MODULE_HTMLPARSER = True
+
+try:
     import warnings
 except ImportError:
-    def deprecation(message):
+    def deprecation(message, stack_offset=0):
         pass
 else:
-    def deprecation(message):
-        warnings.warn(message, DeprecationWarning, stacklevel=2)
+    def deprecation(message, stack_offset=0):
+        warnings.warn(message, DeprecationWarning, stacklevel=3+stack_offset)
 
-VERSION = "0.2.2"
+VERSION = "0.2.9"
 
 CHUNK = 1024  # size of chunks fed to parser, in bytes
 
 DEFAULT_ENCODING = "latin-1"
 
+class Missing: pass
+
 _compress_re = re.compile(r"\s+")
 def compress_text(text): return _compress_re.sub(" ", text.strip())
 
+def normalize_line_endings(text):
+    return re.sub(r"(?:(?<!\r)\n)|(?:\r(?!\n))", "\r\n", text)
+
+
 # This version of urlencode is from my Python 1.5.2 back-port of the
 # Python 2.1 CVS maintenance branch of urllib.  It will accept a sequence
 # of pairs instead of a mapping -- the 2.0 version only accepts a mapping.
@@ -429,10 +444,25 @@
 
 class ItemCountError(ValueError): pass
 
+# for backwards compatibility, ParseError derives from exceptions that were
+# raised by versions of ClientForm <= 0.2.5
+if HAVE_MODULE_HTMLPARSER:
+    SGMLLIB_PARSEERROR = sgmllib.SGMLParseError
+    class ParseError(sgmllib.SGMLParseError,
+                     HTMLParser.HTMLParseError,
+                     ):
+        pass
+else:
+    if hasattr(sgmllib, "SGMLParseError"):
+        SGMLLIB_PARSEERROR = sgmllib.SGMLParseError
+        class ParseError(sgmllib.SGMLParseError):
+            pass
+    else:
+        SGMLLIB_PARSEERROR = RuntimeError
+        class ParseError(RuntimeError):
+            pass
 
-class ParseError(Exception): pass
 
-
 class _AbstractFormParser:
     """forms attribute contains HTMLForm instances on completion."""
     # thanks to Moshe Zadka for an example of sgmllib/htmllib usage
@@ -452,22 +482,29 @@
         self._option = None
         self._textarea = None
 
+        # forms[0] will contain all controls that are outside of any form
+        # self._global_form is an alias for self.forms[0]
+        self._global_form = None
+        self.start_form([])
+        self.end_form()
+        self._current_form = self._global_form = self.forms[0]
+
     def do_base(self, attrs):
         debug("%s", attrs)
         for key, value in attrs:
             if key == "href":
-                self.base = value
+                self.base = self.unescape_attr_if_required(value)
 
     def end_body(self):
         debug("")
         if self._current_label is not None:
             self.end_label()
-        if self._current_form is not None:
+        if self._current_form is not self._global_form:
             self.end_form()
 
     def start_form(self, attrs):
         debug("%s", attrs)
-        if self._current_form is not None:
+        if self._current_form is not self._global_form:
             raise ParseError("nested FORMs")
         name = None
         action = None
@@ -476,14 +513,14 @@
         d = {}
         for key, value in attrs:
             if key == "name":
-                name = value
+                name = self.unescape_attr_if_required(value)
             elif key == "action":
-                action = value
+                action = self.unescape_attr_if_required(value)
             elif key == "method":
-                method = value.upper()
+                method = self.unescape_attr_if_required(value.upper())
             elif key == "enctype":
-                enctype = value.lower()
-            d[key] = value
+                enctype = self.unescape_attr_if_required(value.lower())
+            d[key] = self.unescape_attr_if_required(value)
         controls = []
         self._current_form = (name, action, method, enctype), d, controls
 
@@ -491,22 +528,20 @@
         debug("")
         if self._current_label is not None:
             self.end_label()
-        if self._current_form is None:
+        if self._current_form is self._global_form:
             raise ParseError("end of FORM before start")
         self.forms.append(self._current_form)
-        self._current_form = None
+        self._current_form = self._global_form
 
     def start_select(self, attrs):
         debug("%s", attrs)
-        if self._current_form is None:
-            raise ParseError("start of SELECT before start of FORM")
         if self._select is not None:
             raise ParseError("nested SELECTs")
         if self._textarea is not None:
             raise ParseError("SELECT inside TEXTAREA")
         d = {}
         for key, val in attrs:
-            d[key] = val
+            d[key] = self.unescape_attr_if_required(val)
 
         self._select = d
         self._add_label(d)
@@ -515,8 +550,6 @@
 
     def end_select(self):
         debug("")
-        if self._current_form is None:
-            raise ParseError("end of SELECT before start of FORM")
         if self._select is None:
             raise ParseError("end of SELECT before start")
 
@@ -531,7 +564,7 @@
             raise ParseError("OPTGROUP outside of SELECT")
         d = {}
         for key, val in attrs:
-            d[key] = val
+            d[key] = self.unescape_attr_if_required(val)
 
         self._optgroup = d
 
@@ -550,7 +583,7 @@
 
         d = {}
         for key, val in attrs:
-            d[key] = val
+            d[key] = self.unescape_attr_if_required(val)
 
         self._option = {}
         self._option.update(d)
@@ -583,23 +616,19 @@
 
     def start_textarea(self, attrs):
         debug("%s", attrs)
-        if self._current_form is None:
-            raise ParseError("start of TEXTAREA before start of FORM")
         if self._textarea is not None:
             raise ParseError("nested TEXTAREAs")
         if self._select is not None:
             raise ParseError("TEXTAREA inside SELECT")
         d = {}
         for key, val in attrs:
-            d[key] = val
+            d[key] = self.unescape_attr_if_required(val)
         self._add_label(d)
 
         self._textarea = d
 
     def end_textarea(self):
         debug("")
-        if self._current_form is None:
-            raise ParseError("end of TEXTAREA before start of FORM")
         if self._textarea is None:
             raise ParseError("end of TEXTAREA before start")
         controls = self._current_form[2]
@@ -613,7 +642,7 @@
             self.end_label()
         d = {}
         for key, val in attrs:
-            d[key] = val
+            d[key] = self.unescape_attr_if_required(val)
         taken = bool(d.get("for"))  # empty id is invalid
         d["__text"] = ""
         d["__taken"] = taken
@@ -628,28 +657,28 @@
             # something is ugly in the HTML, but we're ignoring it
             return
         self._current_label = None
-        label["__text"] = label["__text"]
         # if it is staying around, it is True in all cases
         del label["__taken"]
 
     def _add_label(self, d):
         #debug("%s", d)
         if self._current_label is not None:
-            if self._current_label["__taken"]:
-                self.end_label()  # be fuzzy
-            else:
+            if not self._current_label["__taken"]:
                 self._current_label["__taken"] = True
                 d["__label"] = self._current_label
 
     def handle_data(self, data):
+        debug("%s", data)
+
         # according to http://www.w3.org/TR/html4/appendix/notes.html#h-B.3.1
         # line break immediately after start tags or immediately before end
         # tags must be ignored, but real browsers only ignore a line break
         # after a start tag, so we'll do that.
-        if data[0:1] == '\n':
+        if data[0:2] == "\r\n":
+            data = data[2:]
+        if data[0:1] in ["\n", "\r"]:
             data = data[1:]
 
-        debug("%s", data)
         if self._option is not None:
             # self._option is a dictionary of the OPTION element's HTML
             # attributes, but it has two special keys, one of which is the
@@ -660,6 +689,7 @@
         elif self._textarea is not None:
             map = self._textarea
             key = "value"
+            data = normalize_line_endings(data)
         # not if within option or textarea
         elif self._current_label is not None:
             map = self._current_label
@@ -674,12 +704,10 @@
 
     def do_button(self, attrs):
         debug("%s", attrs)
-        if self._current_form is None:
-            raise ParseError("start of BUTTON before start of FORM")
         d = {}
         d["type"] = "submit"  # default
         for key, val in attrs:
-            d[key] = val
+            d[key] = self.unescape_attr_if_required(val)
         controls = self._current_form[2]
 
         type = d["type"]
@@ -694,12 +722,10 @@
 
     def do_input(self, attrs):
         debug("%s", attrs)
-        if self._current_form is None:
-            raise ParseError("start of INPUT before start of FORM")
         d = {}
         d["type"] = "text"  # default
         for key, val in attrs:
-            d[key] = val
+            d[key] = self.unescape_attr_if_required(val)
         controls = self._current_form[2]
 
         type = d["type"]
@@ -709,11 +735,9 @@
 
     def do_isindex(self, attrs):
         debug("%s", attrs)
-        if self._current_form is None:
-            raise ParseError("start of ISINDEX before start of FORM")
         d = {}
         for key, val in attrs:
-            d[key] = val
+            d[key] = self.unescape_attr_if_required(val)
         controls = self._current_form[2]
 
         self._add_label(d)
@@ -750,11 +774,7 @@
     def unknown_charref(self, ref): self.handle_data("&#%s;" % ref)
 
 
-# HTMLParser.HTMLParser is recent, so live without it if it's not available
-# (also, htmllib.HTMLParser is much more tolerant of bad HTML)
-try:
-    import HTMLParser
-except ImportError:
+if not HAVE_MODULE_HTMLPARSER:
     class XHTMLCompatibleFormParser:
         def __init__(self, entitydefs=None, encoding=DEFAULT_ENCODING):
             raise ValueError("HTMLParser could not be imported")
@@ -766,6 +786,12 @@
             HTMLParser.HTMLParser.__init__(self)
             _AbstractFormParser.__init__(self, entitydefs, encoding)
 
+        def feed(self, data):
+            try:
+                HTMLParser.HTMLParser.feed(self, data)
+            except HTMLParser.HTMLParseError, exc:
+                raise ParseError(exc)
+
         def start_option(self, attrs):
             _AbstractFormParser._start_option(self, attrs)
 
@@ -803,31 +829,52 @@
         def unescape_attrs_if_required(self, attrs):
             return attrs  # ditto
 
-import sgmllib
-# monkeypatch to fix http://www.python.org/sf/803422 :-(
-sgmllib.charref = re.compile("&#(x?[0-9a-fA-F]+)[^0-9a-fA-F]")
+
 class _AbstractSgmllibParser(_AbstractFormParser):
+
     def do_option(self, attrs):
         _AbstractFormParser._start_option(self, attrs)
 
-    def unescape_attr_if_required(self, name):
-        return self.unescape_attr(name)
-    def unescape_attrs_if_required(self, attrs):
-        return self.unescape_attrs(attrs)
+    if sys.version_info[:2] >= (2,5):
+        # we override this attr to decode hex charrefs
+        entity_or_charref = re.compile(
+            '&(?:([a-zA-Z][-.a-zA-Z0-9]*)|#(x?[0-9a-fA-F]+))(;?)')
+        def convert_entityref(self, name):
+            return unescape("&%s;" % name, self._entitydefs, self._encoding)
+        def convert_charref(self, name):
+            return unescape_charref("%s" % name, self._encoding)
+        def unescape_attr_if_required(self, name):
+            return name  # sgmllib already did it
+        def unescape_attrs_if_required(self, attrs):
+            return attrs  # ditto
+    else:
+        def unescape_attr_if_required(self, name):
+            return self.unescape_attr(name)
+        def unescape_attrs_if_required(self, attrs):
+            return self.unescape_attrs(attrs)
 
+
 class FormParser(_AbstractSgmllibParser, sgmllib.SGMLParser):
     """Good for tolerance of incorrect HTML, bad for XHTML."""
     def __init__(self, entitydefs=None, encoding=DEFAULT_ENCODING):
         sgmllib.SGMLParser.__init__(self)
         _AbstractFormParser.__init__(self, entitydefs, encoding)
 
-try:
-    if sys.version_info[:2] < (2, 2):
-        raise ImportError  # BeautifulSoup uses generators
-    import BeautifulSoup
-except ImportError:
-    pass
-else:
+    def feed(self, data):
+        try:
+            sgmllib.SGMLParser.feed(self, data)
+        except SGMLLIB_PARSEERROR, exc:
+            raise ParseError(exc)
+
+
+
+# sigh, must support mechanize by allowing dynamic creation of classes based on
+# its bundled copy of BeautifulSoup (which was necessary because of dependency
+# problems)
+
+def _create_bs_classes(bs,
+                       icbinbs,
+                       ):
     class _AbstractBSFormParser(_AbstractSgmllibParser):
         bs_base_class = None
         def __init__(self, entitydefs=None, encoding=DEFAULT_ENCODING):
@@ -836,31 +883,115 @@
         def handle_data(self, data):
             _AbstractFormParser.handle_data(self, data)
             self.bs_base_class.handle_data(self, data)
+        def feed(self, data):
+            try:
+                self.bs_base_class.feed(self, data)
+            except SGMLLIB_PARSEERROR, exc:
+                raise ParseError(exc)
 
-    class RobustFormParser(_AbstractBSFormParser, BeautifulSoup.BeautifulSoup):
+
+    class RobustFormParser(_AbstractBSFormParser, bs):
         """Tries to be highly tolerant of incorrect HTML."""
-        bs_base_class = BeautifulSoup.BeautifulSoup
-    class NestingRobustFormParser(_AbstractBSFormParser,
-                                  BeautifulSoup.ICantBelieveItsBeautifulSoup):
+        pass
+    RobustFormParser.bs_base_class = bs
+    class NestingRobustFormParser(_AbstractBSFormParser, icbinbs):
         """Tries to be highly tolerant of incorrect HTML.
 
         Different from RobustFormParser in that it more often guesses nesting
         above missing end tags (see BeautifulSoup docs).
 
         """
-        bs_base_class = BeautifulSoup.ICantBelieveItsBeautifulSoup
+        pass
+    NestingRobustFormParser.bs_base_class = icbinbs
 
+    return RobustFormParser, NestingRobustFormParser
+
+try:
+    if sys.version_info[:2] < (2, 2):
+        raise ImportError  # BeautifulSoup uses generators
+    import BeautifulSoup
+except ImportError:
+    pass
+else:
+    RobustFormParser, NestingRobustFormParser = _create_bs_classes(
+        BeautifulSoup.BeautifulSoup, BeautifulSoup.ICantBelieveItsBeautifulSoup
+        )
+    __all__ += ['RobustFormParser', 'NestingRobustFormParser']
+
+
 #FormParser = XHTMLCompatibleFormParser  # testing hack
 #FormParser = RobustFormParser  # testing hack
 
-def ParseResponse(response, select_default=False,
-                  ignore_errors=False,  # ignored!
-                  form_parser_class=FormParser,
-                  request_class=urllib2.Request,
-                  entitydefs=None,
-                  backwards_compat=True,
-                  encoding=DEFAULT_ENCODING,
-                  ):
+
+def ParseResponseEx(response,
+                    select_default=False,
+                    form_parser_class=FormParser,
+                    request_class=urllib2.Request,
+                    entitydefs=None,
+                    encoding=DEFAULT_ENCODING,
+
+                    # private
+                    _urljoin=urlparse.urljoin,
+                    _urlparse=urlparse.urlparse,
+                    _urlunparse=urlparse.urlunparse,
+                    ):
+    """Identical to ParseResponse, except that:
+
+    1. The returned list contains an extra item.  The first form in the list
+    contains all controls not contained in any FORM element.
+
+    2. The arguments ignore_errors and backwards_compat have been removed.
+
+    3. Backwards-compatibility mode (backwards_compat=True) is not available.
+    """
+    return _ParseFileEx(response, response.geturl(),
+                        select_default,
+                        False,
+                        form_parser_class,
+                        request_class,
+                        entitydefs,
+                        False,
+                        encoding,
+                        _urljoin=_urljoin,
+                        _urlparse=_urlparse,
+                        _urlunparse=_urlunparse,
+                        )
+
+def ParseFileEx(file, base_uri,
+                select_default=False,
+                form_parser_class=FormParser,
+                request_class=urllib2.Request,
+                entitydefs=None,
+                encoding=DEFAULT_ENCODING,
+
+                # private
+                _urljoin=urlparse.urljoin,
+                _urlparse=urlparse.urlparse,
+                _urlunparse=urlparse.urlunparse,
+                ):
+    """Identical to ParseFile, except that:
+
+    1. The returned list contains an extra item.  The first form in the list
+    contains all controls not contained in any FORM element.
+
+    2. The arguments ignore_errors and backwards_compat have been removed.
+
+    3. Backwards-compatibility mode (backwards_compat=True) is not available.
+    """
+    return _ParseFileEx(file, base_uri,
+                        select_default,
+                        False,
+                        form_parser_class,
+                        request_class,
+                        entitydefs,
+                        False,
+                        encoding,
+                        _urljoin=_urljoin,
+                        _urlparse=_urlparse,
+                        _urlunparse=_urlunparse,
+                        )
+
+def ParseResponse(response, *args, **kwds):
     """Parse HTTP response and return a list of HTMLForm instances.
 
     The return value of urllib2.urlopen can be conveniently passed to this
@@ -920,23 +1051,9 @@
     own risk: there is no well-defined interface.
 
     """
-    return ParseFile(response, response.geturl(), select_default,
-                     False,
-                     form_parser_class,
-                     request_class,
-                     entitydefs,
-                     backwards_compat,
-                     encoding,
-                     )
+    return _ParseFileEx(response, response.geturl(), *args, **kwds)[1:]
 
-def ParseFile(file, base_uri, select_default=False,
-              ignore_errors=False,  # ignored!
-              form_parser_class=FormParser,
-              request_class=urllib2.Request,
-              entitydefs=None,
-              backwards_compat=True,
-              encoding=DEFAULT_ENCODING,
-              ):
+def ParseFile(file, base_uri, *args, **kwds):
     """Parse HTML and return a list of HTMLForm instances.
 
     ClientForm.ParseError is raised on parse errors.
@@ -950,8 +1067,22 @@
     For the other arguments and further details, see ParseResponse.__doc__.
 
     """
+    return _ParseFileEx(file, base_uri, *args, **kwds)[1:]
+
+def _ParseFileEx(file, base_uri,
+                 select_default=False,
+                 ignore_errors=False,
+                 form_parser_class=FormParser,
+                 request_class=urllib2.Request,
+                 entitydefs=None,
+                 backwards_compat=True,
+                 encoding=DEFAULT_ENCODING,
+                 _urljoin=urlparse.urljoin,
+                 _urlparse=urlparse.urlparse,
+                 _urlunparse=urlparse.urlunparse,
+                 ):
     if backwards_compat:
-        deprecation("operating in backwards-compatibility mode")
+        deprecation("operating in backwards-compatibility mode", 1)
     fp = form_parser_class(entitydefs, encoding)
     while 1:
         data = file.read(CHUNK)
@@ -980,21 +1111,18 @@
         if action is None:
             action = base_uri
         else:
-            action = urljoin(base_uri, action)
-        action = fp.unescape_attr_if_required(action)
-        name = fp.unescape_attr_if_required(name)
-        attrs = fp.unescape_attrs_if_required(attrs)
+            action = _urljoin(base_uri, action)
         # would be nice to make HTMLForm class (form builder) pluggable
         form = HTMLForm(
             action, method, enctype, name, attrs, request_class,
             forms, labels, id_to_labels, backwards_compat)
+        form._urlparse = _urlparse
+        form._urlunparse = _urlunparse
         for ii in range(len(controls)):
             type, name, attrs = controls[ii]
-            attrs = fp.unescape_attrs_if_required(attrs)
-            name = fp.unescape_attr_if_required(name)
             # index=ii*10 allows ImageControl to return multiple ordered pairs
-            form.new_control(type, name, attrs, select_default=select_default,
-                             index=ii*10)
+            form.new_control(
+                type, name, attrs, select_default=select_default, index=ii*10)
         forms.append(form)
     for form in forms:
         form.fixup()
@@ -1181,6 +1309,9 @@
 
         self._clicked = False
 
+        self._urlparse = urlparse.urlparse
+        self._urlunparse = urlparse.urlunparse
+
     def __getattr__(self, name):
         if name == "value":
             return self.__dict__["_value"]
@@ -1389,10 +1520,10 @@
         # This doesn't seem to be specified in HTML 4.01 spec. (ISINDEX is
         # deprecated in 4.01, but it should still say how to submit it).
         # Submission of ISINDEX is explained in the HTML 3.2 spec, though.
-        parts = urlparse.urlparse(form.action)
+        parts = self._urlparse(form.action)
         rest, (query, frag) = parts[:-2], parts[-2:]
-        parts = rest + (urllib.quote_plus(self.value), "")
-        url = urlparse.urlunparse(parts)
+        parts = rest + (urllib.quote_plus(self.value), None)
+        url = self._urlunparse(parts)
         req_data = url, None, []
 
         if return_type == "pairs":
@@ -1515,6 +1646,8 @@
         return res
 
     def __repr__(self):
+        # XXX appending the attrs without distinguishing them from name and id
+        # is silly
         attrs = [("name", self.name), ("id", self.id)]+self.attrs.items()
         return "<%s %s>" % (
             self.__class__.__name__,
@@ -1628,6 +1761,7 @@
         self.disabled = False
         self.readonly = False
         self.id = attrs.get("id")
+        self._closed = False
 
         # As Controls are merged in with .merge_control(), self.attrs will
         # refer to each Control in turn -- always the most recently merged
@@ -1856,16 +1990,27 @@
             "control.get(...).attrs")
         return self._get(name, by_label, nr).attrs
 
+    def close_control(self):
+        self._closed = True
+
     def add_to_form(self, form):
         assert self._form is None or form == self._form, (
             "can't add control to more than one form")
         self._form = form
-        try:
-            control = form.find_control(self.name, self.type)
-        except ControlNotFoundError:
+        if self.name is None:
+            # always count nameless elements as separate controls
             Control.add_to_form(self, form)
         else:
-            control.merge_control(self)
+            for ii in range(len(form.controls)-1, -1, -1):
+                control = form.controls[ii]
+                if control.name == self.name and control.type == self.type:
+                    if control._closed:
+                        Control.add_to_form(self, form)
+                    else:
+                        control.merge_control(self)
+                    break
+            else:
+                Control.add_to_form(self, form)
 
     def merge_control(self, control):
         assert bool(control.multiple) == bool(self.multiple)
@@ -1911,6 +2056,8 @@
     def __getattr__(self, name):
         if name == "value":
             compat = self._form.backwards_compat
+            if self.name is None:
+                return []
             return [o.name for o in self.items if o.selected and
                     (not o.disabled or compat)]
         else:
@@ -2080,7 +2227,7 @@
         return [o.name for o in self.items]
 
     def _totally_ordered_pairs(self):
-        if self.disabled:
+        if self.disabled or self.name is None:
             return []
         else:
             return [(o._index, self.name, o.name) for o in self.items
@@ -2163,9 +2310,6 @@
 
     OPTION 'values', in HTML parlance, are Item 'names' in ClientForm parlance.
 
-
-    OPTION 'values', in HTML parlance, are Item 'names' in ClientForm parlance.
-
     SELECT control values and labels are subject to some messy defaulting
     rules.  For example, if the HTML representation of the control is:
 
@@ -2625,6 +2769,9 @@
 
         self.backwards_compat = backwards_compat  # note __setattr__
 
+        self._urlunparse = urlparse.urlunparse
+        self._urlparse = urlparse.urlparse
+
     def __getattr__(self, name):
         if name == "backwards_compat":
             return self._backwards_compat
@@ -2682,7 +2829,17 @@
             control = klass(type, name, a, select_default, index)
         else:
             control = klass(type, name, a, index)
+
+        if type == "select" and len(attrs) == 1:
+            for ii in range(len(self.controls)-1, -1, -1):
+                ctl = self.controls[ii]
+                if ctl.type == "select":
+                    ctl.close_control()
+                    break
+
         control.add_to_form(self)
+        control._urlparse = self._urlparse
+        control._urlunparse = self._urlunparse
 
     def fixup(self):
         """Normalise form after all controls have been added.
@@ -3060,7 +3217,8 @@
                                   is_listcontrol, nr)
 
     def _find_control(self, name, type, kind, id, label, predicate, nr):
-        if (name is not None) and not isstringlike(name):
+        if ((name is not None) and (name is not Missing) and
+            not isstringlike(name)):
             raise TypeError("control name must be string-like")
         if (type is not None) and not isstringlike(type):
             raise TypeError("control type must be string-like")
@@ -3082,7 +3240,8 @@
             nr = 0
 
         for control in self.controls:
-            if name is not None and name != control.name:
+            if ((name is not None and name != control.name) and
+                (name is not Missing or control.name is not None)):
                 continue
             if type is not None and type != control.type:
                 continue
@@ -3112,7 +3271,7 @@
             return found
 
         description = []
-        if name is not None: description.append("name '%s'" % name)
+        if name is not None: description.append("name %s" % repr(name))
         if type is not None: description.append("type '%s'" % type)
         if kind is not None: description.append("kind '%s'" % kind)
         if id is not None: description.append("id '%s'" % id)
@@ -3169,19 +3328,19 @@
         """Return a tuple (url, data, headers)."""
         method = self.method.upper()
         #scheme, netloc, path, parameters, query, frag = urlparse.urlparse(self.action)
-        parts = urlparse.urlparse(self.action)
+        parts = self._urlparse(self.action)
         rest, (query, frag) = parts[:-2], parts[-2:]
 
         if method == "GET":
             if self.enctype != "application/x-www-form-urlencoded":
                 raise ValueError(
                     "unknown GET form encoding type '%s'" % self.enctype)
-            parts = rest + (urlencode(self._pairs()), "")
-            uri = urlparse.urlunparse(parts)
+            parts = rest + (urlencode(self._pairs()), None)
+            uri = self._urlunparse(parts)
             return uri, None, []
         elif method == "POST":
-            parts = rest + (query, "")
-            uri = urlparse.urlunparse(parts)
+            parts = rest + (query, None)
+            uri = self._urlunparse(parts)
             if self.enctype == "application/x-www-form-urlencoded":
                 return (uri, urlencode(self._pairs()),
                         [("Content-type", self.enctype)])



More information about the Zope3-Checkins mailing list