SVN: r25339 - in trunk/quixote: . form2 src test

Neil Schemenauer <nascheme-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Thu, 14 Oct 2004 13:18:06 -0400
Newsgroups gmane.comp.web.quixote.cvs
Message-ID <[email protected]>
Author: nascheme
Date: 2004-10-14 13:17:51 -0400 (Thu, 14 Oct 2004)
New Revision: 25339

Modified:
   trunk/quixote/_py_htmltext.py
   trunk/quixote/errors.py
   trunk/quixote/form2/widget.py
   trunk/quixote/html.py
   trunk/quixote/http_response.py
   trunk/quixote/publish.py
   trunk/quixote/src/_c_htmltext.c
   trunk/quixote/src/setup.py
   trunk/quixote/test/utest_html.py
Log:
Add support for unicode.


Modified: trunk/quixote/_py_htmltext.py
===================================================================
--- trunk/quixote/_py_htmltext.py	2004-10-14 17:10:35 UTC (rev 25338)
+++ trunk/quixote/_py_htmltext.py	2004-10-14 17:17:51 UTC (rev 25339)
@@ -5,37 +5,39 @@
 #$HeadURL$
 #$Id$
 
-import sys
-from types import UnicodeType, TupleType, StringType, IntType, FloatType, \
-    LongType
 import re
 
-if sys.hexversion < 0x20200b1:
-    # 2.2 compatibility hacks
-    class object:
-        pass
-
-    def classof(o):
-        if hasattr(o, "__class__"):
-            return o.__class__
-        else:
-            return type(o)
-
-else:
-    classof = type
-
 _format_codes = 'diouxXeEfFgGcrs%'
 _format_re = re.compile(r'%%[^%s]*[%s]' % (_format_codes, _format_codes))
 
 def _escape_string(s):
-    if not isinstance(s, StringType):
-        raise TypeError, 'string required'
+    if not isinstance(s, basestring):
+        raise TypeError, 'string object required'
     s = s.replace("&", "&amp;")
     s = s.replace("<", "&lt;")
     s = s.replace(">", "&gt;")
     s = s.replace('"', "&quot;")
     return s
 
+def stringify(obj):
+    """Return 'obj' as a string or unicode object.  Tries to prevent
+    turning strings into unicode objects.
+    """
+    if isinstance(obj, basestring):
+        return obj
+    elif hasattr(obj, '__unicode__'):
+        s = obj.__unicode__()
+        if not isinstance(s, basestring):
+            raise TypeError, '__unicode__ did not return a string'
+        return s
+    elif hasattr(obj, '__str__'):
+        s = obj.__str__()
+        if not isinstance(s, basestring):
+            raise TypeError, '__str__ did not return a string'
+        return s
+    else:
+        return str(obj)
+
 class htmltext(object):
     """The htmltext string-like type.  This type serves as a tag
     signifying that HTML special characters do not need to be escaped
@@ -45,7 +47,7 @@
     __slots__ = ['s']
 
     def __init__(self, s):
-        self.s = str(s)
+        self.s = stringify(s)
 
     # XXX make read-only
     #def __setattr__(self, name, value):
@@ -80,39 +82,39 @@
         if usedict:
             args = _DictWrapper(args)
         else:
-            if len(codes) == 1 and not isinstance(args, TupleType):
+            if len(codes) == 1 and not isinstance(args, tuple):
                 args = (args,)
-            args = tuple([_wraparg(arg) for arg in args])
-        return self.__class__(self.s % args)
+            args = tuple(map(_wraparg, args))
+        return htmltext(self.s % args)
 
     def __add__(self, other):
-        if isinstance(other, StringType):
-            return self.__class__(self.s + _escape_string(other))
-        elif classof(other) is self.__class__:
-            return self.__class__(self.s + other.s)
+        if isinstance(other, basestring):
+            return htmltext(self.s + _escape_string(other))
+        elif isinstance(other, htmltext):
+            return htmltext(self.s + other.s)
         else:
             return NotImplemented
 
     def __radd__(self, other):
-        if isinstance(other, StringType):
-            return self.__class__(_escape_string(other) + self.s)
+        if isinstance(other, basestring):
+            return htmltext(_escape_string(other) + self.s)
         else:
             return NotImplemented
 
     def __mul__(self, n):
-        return self.__class__(self.s * n)
+        return htmltext(self.s * n)
 
     def join(self, items):
         quoted_items = []
         for item in items:
-            if classof(item) is self.__class__:
-                quoted_items.append(str(item))
-            elif isinstance(item, StringType):
+            if isinstance(item, htmltext):
+                quoted_items.append(stringify(item))
+            elif isinstance(item, basestring):
                 quoted_items.append(_escape_string(item))
             else:
                 raise TypeError(
                     'join() requires string arguments (got %r)' % item)
-        return self.__class__(self.s.join(quoted_items))
+        return htmltext(self.s.join(quoted_items))
 
     def startswith(self, s):
         if isinstance(s, htmltext):
@@ -137,31 +139,30 @@
             new = new.s
         else:
             new = _escape_string(new)
-        return self.__class__(self.s.replace(old, new))
+        return htmltext(self.s.replace(old, new))
 
     def lower(self):
-        return self.__class__(self.s.lower())
+        return htmltext(self.s.lower())
 
     def upper(self):
-        return self.__class__(self.s.upper())
+        return htmltext(self.s.upper())
 
     def capitalize(self):
-        return self.__class__(self.s.capitalize())
+        return htmltext(self.s.capitalize())
 
 class _QuoteWrapper(object):
     # helper for htmltext class __mod__
 
-    __slots__ = ['value', 'escape']
+    __slots__ = ['value']
 
-    def __init__(self, value, escape):
+    def __init__(self, value):
         self.value = value
-        self.escape = escape
 
     def __str__(self):
-        return self.escape(str(self.value))
+        return _escape_string(stringify(self.value))
 
     def __repr__(self):
-        return self.escape(`self.value`)
+        return _escape_string(`self.value`)
 
 class _DictWrapper(object):
     def __init__(self, value):
@@ -171,15 +172,15 @@
         return _wraparg(self.value[key])
 
 def _wraparg(arg):
-    if (classof(arg) is htmltext or
-        isinstance(arg, IntType) or
-        isinstance(arg, LongType) or
-        isinstance(arg, FloatType)):
+    if (isinstance(arg, htmltext) or
+        isinstance(arg, int) or
+        isinstance(arg, long) or
+        isinstance(arg, float)):
         # ints, longs, floats, and htmltext are okay
         return arg
     else:
         # everything is gets wrapped
-        return _QuoteWrapper(arg, _escape_string)
+        return _QuoteWrapper(arg)
 
 def htmlescape(s):
     """htmlescape(s) -> htmltext
@@ -188,12 +189,10 @@
     already a 'htmltext' object then the HTML markup characters \", <, >,
     and & are first escaped.
     """
-    if classof(s) is htmltext:
+    if isinstance(s, htmltext):
         return s
-    elif isinstance(s,  UnicodeType):
-        s = s.encode('iso-8859-1')
     else:
-        s = str(s)
+        s = stringify(s)
     # inline _escape_string for speed
     s = s.replace("&", "&amp;") # must be done first
     s = s.replace("<", "&lt;")
@@ -222,10 +221,10 @@
                 (self.__class__.__name__, id(self), len(self.data)))
 
     def __str__(self):
-        return str(self.getvalue())
+        return stringify(self.getvalue())
 
     def getvalue(self):
         if self.html:
             return htmltext('').join(map(htmlescape, self.data))
         else:
-            return ''.join(map(str, self.data))
+            return ''.join(map(stringify, self.data))

Modified: trunk/quixote/errors.py
===================================================================
--- trunk/quixote/errors.py	2004-10-14 17:10:35 UTC (rev 25338)
+++ trunk/quixote/errors.py	2004-10-14 17:17:51 UTC (rev 25339)
@@ -45,8 +45,6 @@
 
     def format(self, request):
         msg = htmlescape(self.title)
-        if not isinstance(self.title, htmltext):
-            msg = str(msg) # for backwards compatibility
         if self.public_msg:
             msg = msg + ": " + self.public_msg
         if self.private_msg:
@@ -80,8 +78,6 @@
 
     def format(self, request):
         msg = htmlescape(self.title) + ": " + self.path
-        if not isinstance(self.title, htmltext):
-            msg = str(msg) # for backwards compatibility
         if self.public_msg:
             msg = msg + ": " + self.public_msg
         if self.private_msg:

Modified: trunk/quixote/form2/widget.py
===================================================================
--- trunk/quixote/form2/widget.py	2004-10-14 17:10:35 UTC (rev 25338)
+++ trunk/quixote/form2/widget.py	2004-10-14 17:17:51 UTC (rev 25339)
@@ -8,7 +8,7 @@
 import struct
 from types import FloatType, IntType, ListType, StringType, TupleType
 from quixote import get_request
-from quixote.html import htmltext, htmlescape, htmltag, TemplateIO
+from quixote.html import htmltext, htmlescape, htmltag, TemplateIO, stringify
 from quixote.upload import Upload
 
 def subname(prefix, name):
@@ -40,9 +40,10 @@
         self.msg = msg
 
     def __str__(self):
-        return str(self.msg)
+        return stringify(self.msg)
 
 
+
 class Widget:
     """Abstract base class for web widgets.
 
@@ -125,7 +126,7 @@
                 try:
                     self._parse(request)
                 except WidgetValueError, exc:
-                    self.set_error(str(exc))
+                    self.set_error(stringify(exc))
                 if (self.required and self.value is None and
                     not self.has_error()):
                     self.set_error('required')
@@ -134,7 +135,7 @@
     def _parse(self, request):
         # subclasses may override but this is not part of the public API
         value = request.form.get(self.name)
-        if type(value) is StringType and value.strip():
+        if isinstance(value, basestring) and value.strip():
             self.value = value
         else:
             self.value = None
@@ -376,7 +377,7 @@
                     for value, description, key in options:
                         values.append(value)
                         descriptions.append(description)
-                        keys.append(str(key))
+                        keys.append(stringify(key))
                 else:
                     raise ValueError, 'invalid options %r' % options
             else:
@@ -393,7 +394,7 @@
                     if value is None:
                         return ('', option)
                     else:
-                        return (str(description).lower(), option)
+                        return (stringify(description).lower(), option)
                 doptions = map(make_sort_key, options)
                 doptions.sort()
                 options = [item[1] for item in doptions]

Modified: trunk/quixote/html.py
===================================================================
--- trunk/quixote/html.py	2004-10-14 17:10:35 UTC (rev 25338)
+++ trunk/quixote/html.py	2004-10-14 17:17:51 UTC (rev 25339)
@@ -44,10 +44,10 @@
 try:
     # faster C implementation
     from quixote._c_htmltext import htmltext, htmlescape, _escape_string, \
-        TemplateIO
+        stringify, TemplateIO
 except ImportError:
     from quixote._py_htmltext import htmltext, htmlescape, _escape_string, \
-        TemplateIO
+        stringify, TemplateIO
 
 ValuelessAttr = object() # magic singleton object
 
@@ -97,8 +97,4 @@
             raise ValueError, "value is None and no fallback supplied"
         else:
             return fallback
-    if isinstance(value, unicode):
-        value = value.encode('iso-8859-1')
-    else:
-        value = str(value)
-    return urllib.quote(value)
+    return urllib.quote(stringify(value))

Modified: trunk/quixote/http_response.py
===================================================================
--- trunk/quixote/http_response.py	2004-10-14 17:10:35 UTC (rev 25338)
+++ trunk/quixote/http_response.py	2004-10-14 17:17:51 UTC (rev 25339)
@@ -87,6 +87,8 @@
     after all).
 
     Instance attributes:
+      charset : string
+        the default character encoding of the the response
       status_code : int
         HTTP response status code (integer between 100 and 599)
       reason_phrase : string
@@ -126,10 +128,11 @@
         else's problem.
     """
 
-    def __init__(self, status=200, body=None):
+    def __init__(self, status=200, body=None, charset='iso-8859-1'):
         """
         Creates a new HTTP response.
         """
+        self.charset = charset
         self.set_status(status)
         self.headers = {}
 
@@ -143,6 +146,9 @@
         self.buffered = True
         self.javascript_code = None
 
+    def set_charset(self, charset):
+        self.charset = charset.lower()
+
     def set_status(self, status, reason=None):
         """set_status(status : int, reason : string = None)
 
@@ -208,10 +214,14 @@
             if body.length is not None:
                 self.set_header('content-length', body.length)
         else:
-            self.body = str(body)
+            if self.charset == 'iso-8859-1':
+                self.body = str(body)
+            else:
+                self.body = unicode(body).encode(self.charset)
             self.set_header('content-length', len(self.body))
         if not self.headers.has_key('content-type'):
-            self.set_header('content-type', 'text/html; charset=iso-8859-1')
+            self.set_header('content-type',
+                            'text/html; charset=%s' % self.charset)
 
     def expire_cookie(self, name, **attrs):
         """
@@ -389,6 +399,8 @@
         if self.body is not None:
             if isinstance(self.body, Stream):
                 for chunk in self.body:
+                    if isinstance(chunk, unicode):
+                        chunk = chunk.encode(self.charset)
                     file.write(chunk)
                     if flush_output:
                         file.flush()

Modified: trunk/quixote/publish.py
===================================================================
--- trunk/quixote/publish.py	2004-10-14 17:10:35 UTC (rev 25338)
+++ trunk/quixote/publish.py	2004-10-14 17:17:51 UTC (rev 25339)
@@ -504,10 +504,12 @@
         """Hook for post processing the output.  Subclasses may wish to
         override (e.g. check HTML syntax).
         """
-        if (output and
-                self.config.compress_pages and
-                not isinstance(output, Stream)):
-            output = self.compress_output(request, str(output))
+        if 0:
+            # XXX need to use charset from response
+            if (output and
+                    self.config.compress_pages and
+                    not isinstance(output, Stream)):
+                output = self.compress_output(request, str(output))
         return output
 
     def process_request(self, request, env):
@@ -892,7 +894,7 @@
 
 if sys.hexversion >= 0x02020000:    # Python 2.2 or greater
     def isstring(x):
-        return isinstance(x, (str, unicode, htmltext))
+        return isinstance(x, (basestring, htmltext))
 else:
     if hasattr(types, 'UnicodeType'):
         _string_types = (types.StringType, types.UnicodeType)

Modified: trunk/quixote/src/_c_htmltext.c
===================================================================
--- trunk/quixote/src/_c_htmltext.c	2004-10-14 17:10:35 UTC (rev 25338)
+++ trunk/quixote/src/_c_htmltext.c	2004-10-14 17:17:51 UTC (rev 25339)
@@ -5,7 +5,7 @@
 
 typedef struct {
 	PyObject_HEAD
-	PyStringObject *s;
+	PyObject *s;
 } htmltextObject;
 
 static PyTypeObject htmltext_Type;
@@ -36,10 +36,8 @@
 
 typedef struct {
 	PyObject_HEAD
+	PyObject *data; /* PyList_Object */
 	int html;
-	char *buf;
-	size_t size;
-	size_t pos;
 } TemplateIO_Object;
 
 static PyTypeObject TemplateIO_Type;
@@ -54,19 +52,58 @@
 	return NULL;
 }
 
+static int
+string_check(PyObject *v)
+{
+	return PyUnicode_Check(v) || PyString_Check(v);
+}
+
 static PyObject *
-escape_string(PyObject *s)
+stringify(PyObject *obj)
 {
-	PyObject *new_s;
-	char *ss, *new_ss;
+	static PyObject *unicodestr = NULL;
+	PyObject *res, *func;
+	if (string_check(obj)) {
+		Py_INCREF(obj);
+		return obj;
+	}
+	if (unicodestr == NULL) {
+		unicodestr = PyString_InternFromString("__unicode__");
+		if (unicodestr == NULL)
+			return NULL;
+	}
+	func = PyObject_GetAttr(obj, unicodestr);
+	if (func != NULL) {
+		res = PyEval_CallObject(func, (PyObject *)NULL);
+		Py_DECREF(func);
+	}
+	else {
+		PyErr_Clear();
+		if (obj->ob_type->tp_str != NULL)
+			res = (*obj->ob_type->tp_str)(obj);
+		else
+			res = PyObject_Repr(obj);
+	}
+	if (res == NULL)
+                return NULL;
+	if (!string_check(res)) {
+		Py_DECREF(res);
+		return type_error("string object required");
+	}
+	return res;
+}
+
+static PyObject *
+escape_string(PyObject *obj)
+{
+	char *s;
+	PyObject *newobj;
 	size_t i, j, extra_space, size, new_size;
-	if (!PyString_Check(s))
-		return type_error("str object required");
-	ss = PyString_AS_STRING(s);
-	size = PyString_GET_SIZE(s);
+	assert (PyString_Check(obj));
+	size = PyString_GET_SIZE(obj);
 	extra_space = 0;
 	for (i=0; i < size; i++) {
-		switch (ss[i]) {
+		switch (PyString_AS_STRING(obj)[i]) {
 		case '&':
 			extra_space += 4;
 			break;
@@ -80,53 +117,138 @@
 		}
 	}
 	if (extra_space == 0) {
-		Py_INCREF(s);
-		return (PyObject *)s;
+		Py_INCREF(obj);
+		return (PyObject *)obj;
 	}
 	new_size = size + extra_space;
-	new_s = PyString_FromStringAndSize(NULL, new_size);
-	if (new_s == NULL)
+	newobj = PyString_FromStringAndSize(NULL, new_size);
+	if (newobj == NULL)
 		return NULL;
-	new_ss = PyString_AsString(new_s);
+	s = PyString_AS_STRING(newobj);
 	for (i=0, j=0; i < size; i++) {
-		switch (ss[i]) {
+		switch (PyString_AS_STRING(obj)[i]) {
 		case '&':
-			new_ss[j++] = '&';
-			new_ss[j++] = 'a';
-			new_ss[j++] = 'm';
-			new_ss[j++] = 'p';
-			new_ss[j++] = ';';
+			s[j++] = '&';
+			s[j++] = 'a';
+			s[j++] = 'm';
+			s[j++] = 'p';
+			s[j++] = ';';
 			break;
 		case '<':
-			new_ss[j++] = '&';
-			new_ss[j++] = 'l';
-			new_ss[j++] = 't';
-			new_ss[j++] = ';';
+			s[j++] = '&';
+			s[j++] = 'l';
+			s[j++] = 't';
+			s[j++] = ';';
 			break;
 		case '>':
-			new_ss[j++] = '&';
-			new_ss[j++] = 'g';
-			new_ss[j++] = 't';
-			new_ss[j++] = ';';
+			s[j++] = '&';
+			s[j++] = 'g';
+			s[j++] = 't';
+			s[j++] = ';';
 			break;
 		case '"':
-			new_ss[j++] = '&';
-			new_ss[j++] = 'q';
-			new_ss[j++] = 'u';
-			new_ss[j++] = 'o';
-			new_ss[j++] = 't';
-			new_ss[j++] = ';';
+			s[j++] = '&';
+			s[j++] = 'q';
+			s[j++] = 'u';
+			s[j++] = 'o';
+			s[j++] = 't';
+			s[j++] = ';';
 			break;
 		default:
-			new_ss[j++] = ss[i];
+			s[j++] = PyString_AS_STRING(obj)[i];
 			break;
 		}
 	}
 	assert (j == new_size);
-	return (PyObject *)new_s;
+	return (PyObject *)newobj;
 }
 
 static PyObject *
+escape_unicode(PyObject *obj)
+{
+	Py_UNICODE *u;
+	PyObject *newobj;
+	size_t i, j, extra_space, size, new_size;
+	assert (PyUnicode_Check(obj));
+	size = PyUnicode_GET_SIZE(obj);
+	extra_space = 0;
+	for (i=0; i < size; i++) {
+		switch (PyUnicode_AS_UNICODE(obj)[i]) {
+		case '&':
+			extra_space += 4;
+			break;
+		case '<':
+		case '>':
+			extra_space += 3;
+			break;
+		case '"':
+			extra_space += 5;
+			break;
+		}
+	}
+	if (extra_space == 0) {
+		Py_INCREF(obj);
+		return (PyObject *)obj;
+	}
+	new_size = size + extra_space;
+	newobj = PyUnicode_FromUnicode(NULL, new_size);
+	if (newobj == NULL) {
+		return NULL;
+	}
+	u = PyUnicode_AS_UNICODE(newobj);
+	for (i=0, j=0; i < size; i++) {
+		switch (PyUnicode_AS_UNICODE(obj)[i]) {
+		case '&':
+			u[j++] = '&';
+			u[j++] = 'a';
+			u[j++] = 'm';
+			u[j++] = 'p';
+			u[j++] = ';';
+			break;
+		case '<':
+			u[j++] = '&';
+			u[j++] = 'l';
+			u[j++] = 't';
+			u[j++] = ';';
+			break;
+		case '>':
+			u[j++] = '&';
+			u[j++] = 'g';
+			u[j++] = 't';
+			u[j++] = ';';
+			break;
+		case '"':
+			u[j++] = '&';
+			u[j++] = 'q';
+			u[j++] = 'u';
+			u[j++] = 'o';
+			u[j++] = 't';
+			u[j++] = ';';
+			break;
+		default:
+			u[j++] = PyUnicode_AS_UNICODE(obj)[i];
+			break;
+		}
+	}
+	assert (j == new_size);
+	return (PyObject *)newobj;
+}
+
+static PyObject *
+escape(PyObject *obj)
+{
+	if (PyString_Check(obj)) {
+		return escape_string(obj);
+	}
+	else if (PyUnicode_Check(obj)) {
+		return escape_unicode(obj);
+	}
+	else {
+		return type_error("string object required");
+	}
+}
+
+static PyObject *
 quote_wrapper_new(PyObject *o)
 {
 	QuoteWrapperObject *self;
@@ -160,7 +282,7 @@
 	PyObject *s = PyObject_Repr(self->obj);
 	if (s == NULL)
 		return NULL;
-	qs = escape_string(s);
+	qs = escape(s);
 	Py_DECREF(s);
 	return qs;
 }
@@ -169,10 +291,10 @@
 quote_wrapper_str(QuoteWrapperObject *self)
 {
 	PyObject *qs;
-	PyObject *s = PyObject_Str(self->obj);
+	PyObject *s = stringify(self->obj);
 	if (s == NULL)
 		return NULL;
-	qs = escape_string(s);
+	qs = escape(s);
 	Py_DECREF(s);
 	return qs;
 }
@@ -212,16 +334,17 @@
 static PyObject *
 htmltext_from_string(PyObject *s)
 {
-	/* note, this takes a reference */
+	/* note, this steals a reference */
 	PyObject *self;
 	if (s == NULL)
 		return NULL;
-	assert (PyString_Check(s));
+	assert (string_check(s));
 	self = PyType_GenericAlloc(&htmltext_Type, 0);
 	if (self == NULL) {
+		Py_DECREF(s);
 		return NULL;
 	}
-	((htmltextObject *)self)->s = (PyStringObject *)s;
+	((htmltextObject *)self)->s = s;
 	return self;
 }
 
@@ -234,7 +357,7 @@
 	if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:htmltext", kwlist,
 					 &s))
 		return NULL;
-	s = PyObject_Str(s);
+	s = stringify(s);
 	if (s == NULL)
 		return NULL;
 	self = (htmltextObject *)type->tp_alloc(type, 0);
@@ -242,7 +365,7 @@
 		Py_DECREF(s);
 		return NULL;
 	}
-	self->s = (PyStringObject *)s;
+	self->s = s;
 	return (PyObject *)self;
 }
 
@@ -283,32 +406,19 @@
 static PyObject *
 htmltext_richcompare(PyObject *a, PyObject *b, int op)
 {
-	PyObject *sa, *sb;
-	if (PyString_Check(a)) {
-		sa = a;
-	} else if (htmltextObject_Check(a)) {
-		sa = htmltext_STR(a);
-	} else {
-		goto fail;
+	if (htmltextObject_Check(a)) {
+		a = htmltext_STR(a);
 	}
-	if (PyString_Check(b)) {
-		sb = b;
-	} else if (htmltextObject_Check(b)) {
-		sb = htmltext_STR(b);
-	} else {
-		goto fail;
+	if (htmltextObject_Check(b)) {
+		b = htmltext_STR(b);
 	}
-	return sa->ob_type->tp_richcompare(sa, sb, op);
-
-fail:
-	Py_INCREF(Py_NotImplemented);
-	return Py_NotImplemented;
+	return PyObject_RichCompare(a, b, op);
 }
 
 static long
 htmltext_length(htmltextObject *self)
 {
-	return ((PyStringObject *)self->s)->ob_size;
+	return PyObject_Size(htmltext_STR(self));
 }
 
 
@@ -320,30 +430,42 @@
 		/* don't bother with wrapper object */
 		warg = arg;
 		Py_INCREF(arg);
-	} else {
+	}
+	else {
 		warg = quote_wrapper_new(arg); 
 	}
 	return warg;
 }
 
+
 static PyObject *
 htmltext_format(htmltextObject *self, PyObject *args)
 {
 	/* wrap the format arguments with QuoteWrapperObject */
-	int do_dict = 0;
+	int do_dict = 0, is_unicode;
 	PyObject *rv, *wargs;
+	if (PyUnicode_Check(self->s)) {
+		is_unicode = 1;
+	}
+	else {
+		is_unicode = 0;
+		assert (PyString_Check(self->s));
+	}
 	if (args->ob_type->tp_as_mapping && !PyTuple_Check(args) &&
-	    !PyString_Check(args)) {
-		char *fmt = PyString_AS_STRING(htmltext_STR(self));
-		size_t i, n = PyString_GET_SIZE(htmltext_STR(self));
-		char last = 0;
+	    !string_check(args)) {
+		Py_UNICODE fmt_char, last_char = 0;
+		size_t i, n = PyObject_Size(self->s);
 		/* second check necessary since '%s' % {} => '{}' */
 		for (i=0; i < n; i++) {
-			if (last == '%' && fmt[i] == '(') {
+			if (is_unicode)
+				fmt_char = PyUnicode_AS_UNICODE(self->s)[i];
+			else
+				fmt_char = PyString_AS_STRING(self->s)[i];
+			if (last_char == '%' && fmt_char == '(') {
 				do_dict = 1;
 				break;
 			}
-			last = fmt[i];
+			last_char = fmt_char;
 		}
 	}
 	if (do_dict) {
@@ -369,7 +491,10 @@
 			return NULL;
 		}
 	}
-	rv = PyString_Format((PyObject *)self->s, wargs);
+	if (is_unicode)
+		rv = PyUnicode_Format(self->s, wargs);
+	else
+		rv = PyString_Format(self->s, wargs);
 	Py_DECREF(wargs);
 	return htmltext_from_string(rv);
 }
@@ -377,24 +502,24 @@
 static PyObject *
 htmltext_add(PyObject *v, PyObject *w)
 {
-	PyObject *qv, *qw;
+	PyObject *qv, *qw, *rv;
 	if (htmltextObject_Check(v) && htmltextObject_Check(w)) {
 		qv = htmltext_STR(v);
 		qw = htmltext_STR(w);
 		Py_INCREF(qv);
 		Py_INCREF(qw);
 	}
-	else if (PyString_Check(w)) {
+	else if (string_check(w)) {
 		assert (htmltextObject_Check(v));
 		qv = htmltext_STR(v);
-		qw = escape_string(w);
+		qw = escape(w);
 		if (qw == NULL)
 			return NULL;
 		Py_INCREF(qv);
 	}
-	else if (PyString_Check(v)) {
+	else if (string_check(v)) {
 		assert (htmltextObject_Check(w));
-		qv = escape_string(v);
+		qv = escape(v);
 		if (qv == NULL)
 			return NULL;
 		qw = htmltext_STR(w);
@@ -404,8 +529,10 @@
 		Py_INCREF(Py_NotImplemented);
 		return Py_NotImplemented;
 	}
-	PyString_ConcatAndDel(&qv, qw);
-	return htmltext_from_string(qv);
+	rv = PyUnicode_Concat(qv, qw);
+	Py_DECREF(qv);
+	Py_DECREF(qw);
+	return htmltext_from_string(rv);
 }
 
 static PyObject *
@@ -420,44 +547,46 @@
 static PyObject *
 htmltext_join(PyObject *self, PyObject *args)
 {
-	long i;
-	PyObject *qargs, *rv;
-	if (!PySequence_Check(args)) {
-		return type_error("argument must be a sequence");
-	}
-	qargs = PyList_New(PySequence_Size(args));
-	if (qargs == NULL)
+	int i;
+	PyObject *quoted_args, *rv;
+
+	quoted_args = PySequence_List(args);
+	if (quoted_args == NULL)
 		return NULL;
-	for (i=0; i < PySequence_Size(args); i++) {
+	for (i=0; i < PyList_Size(quoted_args); i++) {
 		PyObject *value, *qvalue;
-		value = PySequence_GetItem(args, i);
+		value = PyList_GET_ITEM(args, i);
 		if (value == NULL) {
 			goto error;
 		}
 		if (htmltextObject_Check(value)) {
 			qvalue = htmltext_STR(value);
 			Py_INCREF(qvalue);
-			Py_DECREF(value);
 		}
-		else if (PyString_Check(value)) {
-			qvalue = escape_string(value);
-			Py_DECREF(value);
-		}
 		else {
-			Py_DECREF(value);
-			type_error("join requires a list of strings");
-			goto error;
+			if (!string_check(value)) {
+				type_error("join requires a list of strings");
+				goto error;
+			}
+			qvalue = escape(value);
+			if (qvalue == NULL)
+				goto error;
 		}
-		if (PyList_SetItem(qargs, i, qvalue) < 0) {
+		if (PyList_SetItem(quoted_args, i, qvalue) < 0) {
 			goto error;
 		}
 	}
-	rv = _PyString_Join(htmltext_STR(self), qargs);
-	Py_DECREF(qargs);
+	if (PyUnicode_Check(htmltext_STR(self))) {
+		rv = PyUnicode_Join(htmltext_STR(self), quoted_args);
+	}
+	else {
+		rv = _PyString_Join(htmltext_STR(self), quoted_args);
+	}
+	Py_DECREF(quoted_args);
 	return htmltext_from_string(rv);
 
 error:
-	Py_DECREF(qargs);
+	Py_DECREF(quoted_args);
 	return NULL;
 }
 
@@ -465,8 +594,8 @@
 quote_arg(PyObject *s)
 {
 	PyObject *ss;
-	if (PyString_Check(s)) {
-		ss = escape_string(s);
+	if (string_check(s)) {
+		ss = escape(s);
 		if (ss == NULL)
 			return NULL;
 	}
@@ -561,25 +690,32 @@
 	if (self == NULL) {
 		return NULL;
 	}
+	self->data = PyList_New(0);
+	if (self->data == NULL) {
+		Py_DECREF(self);
+		return NULL;
+	}
 	self->html = html != 0;
-	self->buf = NULL;
-	self->size = 0;
-	self->pos = 0;
 	return (PyObject *)self;
 }
 
 static void
 template_io_dealloc(TemplateIO_Object *self)
 {
-	if (self->size > 0)
-		PyMem_Free(self->buf);
+	Py_DECREF(self->data);
 	self->ob_type->tp_free((PyObject *)self);
 }
 
 static PyObject *
 template_io_str(TemplateIO_Object *self)
 {
-	return PyString_FromStringAndSize(self->buf, self->pos);
+	static PyObject *empty = NULL;
+	if (empty == NULL) {
+		empty = PyString_FromStringAndSize(NULL, 0);
+		if (empty == NULL)
+			return NULL;
+	}
+	return _PyString_Join(empty, self->data);
 }
 
 static PyObject *
@@ -594,51 +730,8 @@
 }
 
 static PyObject *
-template_io_repr(TemplateIO_Object *self)
-{
-	PyObject *s, *sr, *rv;
-	s = template_io_str(self);
-	if (s == NULL)
-		return NULL;
-	sr = PyObject_Repr(s);
-	Py_DECREF(s);
-	if (sr == NULL)
-		return NULL;
-	rv = PyString_FromFormat("<TemplateIO %s>", PyString_AsString(sr));
-	Py_DECREF(sr);
-	return rv;
-}
-
-
-static PyObject *
-template_io_do_concat(TemplateIO_Object *self, char *s, size_t size)
-{
-	/* note this adds a reference to self */
-	if (self->pos + size > self->size) {
-		size_t new_size;
-		char *new_buf;
-		if (self->size > size)
-			new_size = self->size * 2;
-		else
-			new_size = size * 2;
-		new_buf = PyMem_Realloc(self->buf, new_size);
-		if (new_buf == NULL)
-			return NULL;
-		self->buf = new_buf;
-		self->size = new_size;
-	}
-	assert (self->pos + size <= self->size);
-	memcpy(self->buf + self->pos, s, size);
-	self->pos += size;
-	Py_INCREF(self);
-	return (PyObject *)self;
-}
-	
-
-static PyObject *
 template_io_iadd(TemplateIO_Object *self, PyObject *other)
 {
-	PyObject *rv;
 	PyObject *s = NULL;
 	if (!TemplateIO_Check(self))
 		return type_error("TemplateIO object required");
@@ -646,43 +739,29 @@
 		Py_INCREF(self);
 		return (PyObject *)self;
 	}
-	else if (TemplateIO_Check(other)) {
-		TemplateIO_Object *o = (TemplateIO_Object *)other;
-		if (self->html && !o->html) {
-			PyObject *ss = PyString_FromStringAndSize(o->buf,
-								  o->pos);
-			if (ss == NULL)
-				return NULL;
-			s = escape_string(ss);
-			Py_DECREF(ss);
-			goto concat_str;
-		}
-		rv = template_io_do_concat(self, o->buf, o->pos);
-	}
 	else if (htmltextObject_Check(other)) {
-		PyStringObject *s = ((htmltextObject *)other)->s;
-		rv = template_io_do_concat(self,
-					   PyString_AS_STRING(s),
-					   PyString_GET_SIZE(s));
+		s = htmltext_STR(other);
+		Py_INCREF(s);
 	}
 	else {
 		if (self->html) {
-			PyObject *ss = PyObject_Str(other);
+			PyObject *ss = stringify(other);
 			if (ss == NULL)
 				return NULL;
-			s = escape_string(ss);
+			s = escape(ss);
 			Py_DECREF(ss);
-		} else {
-			s = PyObject_Str(other);
 		}
-concat_str:
+		else {
+			s = stringify(other);
+		}
 		if (s == NULL)
 			return NULL;
-		rv = template_io_do_concat(self, PyString_AS_STRING(s),
-					   PyString_GET_SIZE(s));
-		Py_XDECREF(s);
 	}
-	return rv;
+	if (PyList_Append(self->data, s) != 0)
+		return NULL;
+	Py_DECREF(s);
+	Py_INCREF(self);
+	return (PyObject *)self;
 }
 
 static PyMethodDef htmltext_methods[] = {
@@ -828,9 +907,9 @@
 };
 
 static PyMappingMethods dict_wrapper_as_mapping = {
-        0, /*mp_length*/
-        (binaryfunc)dict_wrapper_subscript, /*mp_subscript*/
-        0, /*mp_ass_subscript*/
+	0, /*mp_length*/
+	(binaryfunc)dict_wrapper_subscript, /*mp_subscript*/
+	0, /*mp_ass_subscript*/
 };
 
 static PyTypeObject DictWrapper_Type = {
@@ -895,7 +974,7 @@
 	0,			/*tp_getattr*/
 	0,			/*tp_setattr*/
 	0,			/*tp_compare*/
-	(unaryfunc)template_io_repr,/*tp_repr*/
+	0,			/*tp_repr*/
 	&template_io_as_number,	/*tp_as_number*/
 	0,			/*tp_as_sequence*/
 	0,			/*tp_as_mapping*/
@@ -939,10 +1018,10 @@
 	}
 	else {
 		PyObject *rv;
-		PyObject *s = PyObject_Str(o);
+		PyObject *s = stringify(o);
 		if (s == NULL)
 			return NULL;
-		rv = escape_string(s);
+		rv = escape(s);
 		Py_DECREF(s);
 		return htmltext_from_string(rv);
 	}
@@ -951,18 +1030,21 @@
 static PyObject *
 py_escape_string(PyObject *self, PyObject *o)
 {
-	PyObject *rv;
-	if (!PyString_Check(o))
-		return type_error("string required");
-	rv = escape_string(o);
-	return rv;
+	return escape(o);
 }
 
+static PyObject *
+py_stringify(PyObject *self, PyObject *o)
+{
+	return stringify(o);
+}
+
 /* List of functions defined in the module */
 
 static PyMethodDef htmltext_module_methods[] = {
 	{"htmlescape",		(PyCFunction)html_escape, METH_O},
 	{"_escape_string",	(PyCFunction)py_escape_string, METH_O},
+	{"stringify",	        (PyCFunction)py_stringify, METH_O},
 	{NULL,			NULL}
 };
 

Modified: trunk/quixote/src/setup.py
===================================================================
--- trunk/quixote/src/setup.py	2004-10-14 17:10:35 UTC (rev 25338)
+++ trunk/quixote/src/setup.py	2004-10-14 17:17:51 UTC (rev 25339)
@@ -4,11 +4,13 @@
 
 Import = Extension(name="cimport",
                       sources=["cimport.c"])
+Html = Extension(name="_c_htmltext",
+                      sources=["_c_htmltext.c"])
 
 setup(name = "cimport",
       version = "0.1",
       description = "Import tools for Python",
       author = "Neil Schemenauer",
       author_email = "nas-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]",
-      ext_modules = [Import]
+      ext_modules = [Import, Html]
       )

Modified: trunk/quixote/test/utest_html.py
===================================================================
--- trunk/quixote/test/utest_html.py	2004-10-14 17:10:35 UTC (rev 25338)
+++ trunk/quixote/test/utest_html.py	2004-10-14 17:17:51 UTC (rev 25339)
@@ -1,12 +1,6 @@
-#!/www/python/bin/python
-"""
-$URL$
-$Id$
-"""
 from sancho.utest import UTest
 from quixote import _py_htmltext
 
-escape = htmlescape = None # so that checker does not complain
 
 class Wrapper:
     def __init__(self, s):
@@ -18,43 +12,51 @@
     def __str__(self):
         return self.s
 
+class BrokenError(Exception):
+    pass
+
 class Broken:
     def __str__(self):
-        raise RuntimeError, 'eieee'
+        raise BrokenError, 'eieee'
 
     def __repr__(self):
-        raise RuntimeError, 'eieee'
+        raise BrokenError, 'eieee'
 
 markupchars = '<>&"'
 quotedchars = '&lt;&gt;&amp;&quot;'
+unicodechars = u'\u1234'
 
 class HTMLTest (UTest):
 
     def _pre(self):
-        global htmltext, escape, htmlescape
+        global htmltext, escape, htmlescape, TemplateIO
         htmltext = _py_htmltext.htmltext
         escape = _py_htmltext._escape_string
         htmlescape = _py_htmltext.htmlescape
+        TemplateIO = _py_htmltext.TemplateIO
 
     def _post(self):
         pass
 
-
-    def check_init(self):
+    def _check_init(self):
         assert str(htmltext('foo')) == 'foo'
         assert str(htmltext(markupchars)) == markupchars
+        assert unicode(htmltext(unicodechars)) == unicodechars
+        assert str(htmltext(unicode(markupchars))) == markupchars
         assert str(htmltext(None)) == 'None'
         assert str(htmltext(1)) == '1'
         try:
             htmltext(Broken())
             assert 0
-        except RuntimeError: pass
+        except BrokenError: pass
 
     def check_escape(self):
         assert htmlescape(markupchars) == quotedchars
         assert isinstance(htmlescape(markupchars), htmltext)
         assert escape(markupchars) == quotedchars
-        assert isinstance(escape(markupchars), str)
+        assert escape(unicodechars) == unicodechars
+        assert escape(unicode(markupchars)) == quotedchars
+        assert isinstance(escape(markupchars), basestring)
         assert htmlescape(htmlescape(markupchars)) == quotedchars
         try:
             escape(1)
@@ -67,6 +69,7 @@
         assert s != 'bar'
         assert s == htmltext('foo')
         assert s != htmltext('bar')
+        assert htmltext(u'\u1234') == u'\u1234'
         assert htmltext('1') != 1
         assert 1 != s
 
@@ -89,6 +92,7 @@
         assert isinstance(s + markupchars, htmltext)
         assert markupchars + s == quotedchars + "foo"
         assert isinstance(markupchars + s, htmltext)
+        assert markupchars + htmltext(u'') == quotedchars
         try:
             s + 1
             assert 0
@@ -118,23 +122,29 @@
 
     def check_format(self):
         s_fmt = htmltext('%s')
+        u_fmt = htmltext(u'%s')
         assert s_fmt % 'foo' == "foo"
+        assert u_fmt % 'foo' == u"foo"
         assert isinstance(s_fmt % 'foo', htmltext)
+        assert isinstance(u_fmt % 'foo', htmltext)
         assert s_fmt % markupchars == quotedchars
+        assert u_fmt % markupchars == quotedchars
         assert s_fmt % None == "None"
+        assert u_fmt % None == "None"
+        assert u_fmt % unicodechars == unicodechars
         assert htmltext('%r') % Wrapper(markupchars) == quotedchars
-        assert htmltext('%s%s') % ('foo', htmltext(markupchars)) == (
-            "foo" + markupchars)
+        assert htmltext('%s%s') % ('foo', htmltext(markupchars)) \
+            == ("foo" + markupchars)
         assert htmltext('%d') % 10 == "10"
         assert htmltext('%.1f') % 10 == "10.0"
         try:
             s_fmt % Broken()
             assert 0
-        except RuntimeError: pass
+        except BrokenError: pass
         try:
             htmltext('%r') % Broken()
             assert 0
-        except RuntimeError: pass
+        except BrokenError: pass
         try:
             s_fmt % (1, 2)
             assert 0
@@ -142,8 +152,9 @@
         assert htmltext('%d') % 12300000000000000000L == "12300000000000000000"
 
     def check_dict_format(self):
-        assert htmltext('%(a)s %(a)r %(b)s') % (
-            {'a': 'foo&', 'b': htmltext('bar&')}) == "foo&amp; 'foo&amp;' bar&"
+        args = {'a': 'foo&', 'b': htmltext('bar&')}
+        result = "foo&amp; 'foo&amp;' bar&"
+        assert htmltext('%(a)s %(a)r %(b)s') % args == result
         assert htmltext('%(a)s') % {'a': 'foo&'} == "foo&amp;"
         assert isinstance(htmltext('%(a)s') % {'a': 'a'}, htmltext)
         assert htmltext('%s') % {'a': 'foo&'} == "{'a': 'foo&amp;'}"
@@ -158,13 +169,15 @@
 
     def check_join(self):
         assert htmltext(' ').join(['foo', 'bar']) == "foo bar"
-        assert htmltext(' ').join(['foo', markupchars]) == (
-            "foo " + quotedchars)
-        assert htmlescape(markupchars).join(['foo', 'bar']) == (
-            "foo" + quotedchars + "bar")
-        assert htmltext(' ').join([htmltext(markupchars), 'bar']) == (
-            markupchars + " bar")
+        assert htmltext(' ').join(['foo', markupchars]) == \
+            "foo " + quotedchars
+        assert htmlescape(markupchars).join(['foo', 'bar']) == \
+            "foo" + quotedchars + "bar"
+        assert htmltext(' ').join([htmltext(markupchars), 'bar']) == \
+            markupchars + " bar"
         assert isinstance(htmltext('').join([]), htmltext)
+        assert htmltext(u' ').join([unicodechars]) == unicodechars
+        assert htmltext(u' ').join(['']) == u''
         try:
             htmltext('').join(1)
             assert 0
@@ -215,6 +228,53 @@
         assert isinstance(htmltext('a').capitalize(), htmltext)
 
 
+class TemplateTest (UTest):
+
+    def _pre(self):
+        global TemplateIO
+        TemplateIO = _py_htmltext.TemplateIO
+
+    def _post(self):
+        pass
+
+    def check_init(self):
+        TemplateIO()
+        TemplateIO(html=True)
+        TemplateIO(html=False)
+
+    def check_text_iadd(self):
+        t = TemplateIO()
+        assert t.getvalue() == ''
+        t += "abcd"
+        assert t.getvalue() == 'abcd'
+        t += None
+        assert t.getvalue() == 'abcd'
+        t += 123
+        assert t.getvalue() == 'abcd123'
+        t += u'\u1234'
+        assert t.getvalue() == u'abcd123\u1234'
+        try:
+            t += Broken(); t.getvalue()
+            assert 0
+        except BrokenError: pass
+
+    def check_html_iadd(self):
+        t = TemplateIO(html=1)
+        assert t.getvalue() == ''
+        t += "abcd"
+        assert t.getvalue() == 'abcd'
+        t += None
+        assert t.getvalue() == 'abcd'
+        t += 123
+        assert t.getvalue() == 'abcd123'
+        try:
+            t += Broken(); t.getvalue()
+            assert 0
+        except BrokenError: pass
+        t = TemplateIO(html=1)
+        t += markupchars
+        assert t.getvalue() == quotedchars
+
 try:
     from quixote import _c_htmltext
 except ImportError:
@@ -230,5 +290,15 @@
             escape = _c_htmltext._escape_string
             htmlescape = _c_htmltext.htmlescape
 
+    class CTemplateTest(TemplateTest):
+        def _pre(self):
+            global TemplateIO
+            TemplateIO = _c_htmltext.TemplateIO
+
+
 if __name__ == "__main__":
     HTMLTest()
+    TemplateTest()
+    if _c_htmltext:
+        CHTMLTest()
+        CTemplateTest()