Skip to content
This repository

html updates #139

Merged
merged 13 commits into from almost 2 years ago

1 participant

Burak Arslan
This page is out of date. Refresh to see the latest.
3  .gitignore
@@ -13,6 +13,9 @@ MANIFEST
13 13
 dist
14 14
 build
15 15
 
  16
+# 2to3 cruft
  17
+*.bak
  18
+
16 19
 # Mac cruft
17 20
 .DS_Store
18 21
 
2  CHANGELOG.rst
Source Rendered
@@ -31,6 +31,8 @@ rpclib-2.8.0-beta
31 31
     * https://github.com/arskom/rpclib/pull/129
32 32
     * https://github.com/arskom/rpclib/pull/133
33 33
     * https://github.com/arskom/rpclib/pull/137
  34
+    * https://github.com/arskom/rpclib/pull/138
  35
+    * https://github.com/arskom/rpclib/pull/139
34 36
 
35 37
 rpclib-2.7.0-beta
36 38
 -----------------
4  src/rpclib/_base.py
@@ -198,6 +198,10 @@ def __init__(self, transport):
198 198
         self.function = None
199 199
         """The callable of the user code."""
200 200
 
  201
+        self.locale = None
  202
+        """The locale the request will use when needed for things like date
  203
+        formatting, html rendering and such."""
  204
+
201 205
         self.frozen = True
202 206
         """When this is set, no new attribute can be added to this class
203 207
         instance. This is mostly for internal use.
9  src/rpclib/model/_base.py
@@ -19,6 +19,7 @@
19 19
 
20 20
 import rpclib.const.xml_ns
21 21
 
  22
+
22 23
 """This module contains the ModelBase class and other building blocks for
23 24
 defining models.
24 25
 """
@@ -34,6 +35,7 @@ def wrapper(cls, element):
34 35
             return func(cls, element)
35 36
     return wrapper
36 37
 
  38
+
37 39
 def nillable_string(func):
38 40
     """Decorator that retuns None if input is None."""
39 41
 
@@ -44,6 +46,7 @@ def wrapper(cls, string):
44 46
             return func(cls, string)
45 47
     return wrapper
46 48
 
  49
+
47 50
 def nillable_iterable(func):
48 51
     """Decorator that retuns [] if input is None."""
49 52
 
@@ -93,6 +96,12 @@ class Attributes(object):
93 96
         """The tag used to add a primitives as child to a complex type in the
94 97
         xml schema."""
95 98
 
  99
+        translations = {}
  100
+        """A dict that contains locale codes keys and translations of field
  101
+        names to human language as a basestring child as values.
  102
+        """
  103
+
  104
+
96 105
     class Annotations(object):
97 106
         """The class that holds the annotations for the given type."""
98 107
 
9  src/rpclib/model/complex.py
@@ -28,10 +28,12 @@
28 28
 from rpclib.model import nillable_dict
29 29
 from rpclib.model import nillable_string
30 30
 
31  
-from rpclib.util.odict import odict as TypeInfo
  31
+from rpclib.util.odict import odict
32 32
 from rpclib.const import xml_ns as namespace
33 33
 from rpclib.const.suffix import TYPE_SUFFIX
34 34
 
  35
+class TypeInfo(odict):
  36
+    pass
35 37
 
36 38
 class _SimpleTypeInfoElement(object):
37 39
     __slots__ = ['path', 'parent', 'type']
@@ -210,6 +212,7 @@ def get_serialization_instance(cls, value):
210 212
             * A dict of native types
211 213
             * The native type itself.
212 214
         """
  215
+
213 216
         # if the instance is a list, convert it to a cls instance.
214 217
         # this is only useful when deserializing method arguments for a client
215 218
         # request which is the only time when the member order is not arbitrary
@@ -276,7 +279,7 @@ def get_flat_type_info(cls, retval=None):
276 279
         """
277 280
 
278 281
         if retval is None:
279  
-            retval = {}
  282
+            retval = TypeInfo()
280 283
 
281 284
         parent = getattr(cls, '__extends__', None)
282 285
         if parent != None:
@@ -304,7 +307,7 @@ def get_simple_type_info(cls, hier_delim="_", retval=None, prefix=None,
304 307
         """
305 308
 
306 309
         if retval is None:
307  
-            retval = {}
  310
+            retval = TypeInfo()
308 311
         if prefix is None:
309 312
             prefix = []
310 313
 
9  src/rpclib/model/primitive.py
@@ -214,10 +214,14 @@ def from_string(cls, value):
214 214
 if sys.version > '3':
215 215
     String = Unicode
216 216
 
217  
-class AnyUri(Unicode):
218  
-    """This is an xml schema type with is a special kind of String."""
  217
+# FIXME: Support this for soft validation
  218
+class AnyUri(String):
  219
+    """A special kind of String type designed to hold an uri."""
219 220
     __type_name__ = 'anyURI'
220 221
 
  222
+class ImageUri(AnyUri):
  223
+    """A special kind of String that holds an uri of an image."""
  224
+
221 225
 class Decimal(SimpleModel):
222 226
     """The primitive that corresponds to the native python Decimal.
223 227
 
@@ -320,7 +324,6 @@ class Integer(Decimal):
320 324
     @classmethod
321 325
     @nillable_string
322 326
     def to_string(cls, value):
323  
-        assert (cls.__length__ is None) or (0 <= value < 2** cls.__length__)
324 327
         int(value) # sanity check
325 328
 
326 329
         return str(value)
370  src/rpclib/protocol/html.py
@@ -18,7 +18,9 @@
18 18
 #
19 19
 
20 20
 """This module contains the EXPERIMENTAL Html protocol implementation.
21  
-It seeks to eliminate the need for templates.
  21
+It seeks to eliminate the need for html templates.
  22
+
  23
+As you can tell, I haven't figured it all out yet :)
22 24
 """
23 25
 
24 26
 import logging
@@ -33,9 +35,17 @@
33 35
 from rpclib.model.binary import ByteArray
34 36
 from rpclib.model.binary import Attachment
35 37
 from rpclib.model.complex import ComplexModelBase
  38
+from rpclib.model.primitive import ImageUri
36 39
 from rpclib.protocol import ProtocolBase
37 40
 from rpclib.util.cdict import cdict
38 41
 
  42
+def translate(cls, locale, default):
  43
+    print locale, cls, cls.Attributes.translations
  44
+    retval = cls.Attributes.translations.get(locale, None)
  45
+    if retval is None:
  46
+        return default
  47
+    return retval
  48
+
39 49
 def serialize_null(prot, cls, name):
40 50
     return [ E(prot.child_tag, **{prot.field_name_attr: name}) ]
41 51
 
@@ -88,15 +98,13 @@ def serialize(self, ctx, message):
88 98
         ctx.out_body_doc, ctx.out_header_doc and ctx.out_document.
89 99
         """
90 100
 
91  
-        assert message in (self.RESPONSE,)
  101
+        assert message in (self.RESPONSE, )
92 102
 
93 103
         self.event_manager.fire_event('before_serialize', ctx)
94 104
 
95 105
         if ctx.out_error is not None:
96 106
             # FIXME: There's no way to alter soap response headers for the user.
97  
-            ctx.out_document = self.serialize_complex_model(
98  
-                    ctx.out_error.__class__, ctx.out_error,
99  
-                    ctx.out_error.get_type_name())
  107
+            ctx.out_document = [ctx.out_error.to_string(ctx.out_error)]
100 108
 
101 109
         else:
102 110
             # instantiate the result message
@@ -112,7 +120,7 @@ def serialize(self, ctx, message):
112 120
 
113 121
             ctx.out_header_doc = None
114 122
             ctx.out_body_doc = self.serialize_impl(result_message_class,
115  
-                                                                 result_message)
  123
+                                                   result_message, ctx.locale)
116 124
 
117 125
             ctx.out_document = ctx.out_body_doc
118 126
 
@@ -143,7 +151,7 @@ class HtmlMicroFormat(HtmlBase):
143 151
     mime_type = 'text/html'
144 152
 
145 153
     def __init__(self, app=None, validator=None, root_tag='div',
146  
-            child_tag='div', field_name_attr='class', skip_depth=0):
  154
+                 child_tag='div', field_name_attr='class', skip_depth=0):
147 155
         """Protocol that returns the response object as a html microformat. See
148 156
         https://en.wikipedia.org/wiki/Microformats for more info.
149 157
 
@@ -164,9 +172,9 @@ def __init__(self, app=None, validator=None, root_tag='div',
164 172
 
165 173
         HtmlBase.__init__(self, app, validator, skip_depth)
166 174
 
167  
-        assert root_tag in ('div','span')
168  
-        assert child_tag in ('div','span')
169  
-        assert field_name_attr in ('class','id')
  175
+        assert root_tag in ('div', 'span')
  176
+        assert child_tag in ('div', 'span')
  177
+        assert field_name_attr in ('class', 'id')
170 178
 
171 179
         self.__root_tag = root_tag
172 180
         self.__child_tag = child_tag
@@ -192,11 +200,12 @@ def field_name_attr(self):
192 200
         return self.__field_name_attr
193 201
 
194 202
     @nillable_value
195  
-    def serialize_model_base(self, cls, value, name='retval'):
196  
-        return [ E(self.child_tag, cls.to_string(value), **{self.field_name_attr: name}) ]
  203
+    def serialize_model_base(self, cls, value, locale, name='retval'):
  204
+        return [ E(self.child_tag, cls.to_string(value),
  205
+                                                **{self.field_name_attr: name}) ]
197 206
 
198  
-    def serialize_impl(self, cls, value):
199  
-        return self.serialize_complex_model(cls, value, cls.get_type_name())
  207
+    def serialize_impl(self, cls, value, locale):
  208
+        return self.serialize_complex_model(cls, value, cls.get_type_name(), locale)
200 209
 
201 210
     @nillable_value
202 211
     def serialize_complex_model(self, cls, value, name='retval'):
@@ -214,46 +223,83 @@ def serialize_complex_model(self, cls, value, name='retval'):
214 223
         yield '</%s>' % self.root_tag
215 224
 
216 225
 
217  
-class HtmlTable(HtmlBase):
  226
+def HtmlTable(app=None, validator=None, produce_header=True,
  227
+                    table_name_attr='class', field_name_attr=None, border=0,
  228
+                        fields_as='columns', row_class=None, cell_class=None,
  229
+                                                        header_cell_class=None):
  230
+    """Protocol that returns the response object as a html table.
  231
+
  232
+    The simple flavour is like the HtmlMicroFormatprotocol, but returns data
  233
+    as a html table using the <table> tag.
  234
+
  235
+    :param app: A rpclib.application.Application instance.
  236
+    :param validator: The validator to use. Ignored.
  237
+    :param produce_header: Boolean value to determine whether to show field
  238
+        names in the beginning of the table or not. Defaults to True. Set to
  239
+        False to skip headers.
  240
+    :param table_name_attr: The name of the attribute that will contain the
  241
+        response name of the complex object in the table tag. Set to None to
  242
+        disable.
  243
+    :param field_name_attr: The name of the attribute that will contain the
  244
+        field names of the complex object children for every table cell. Set
  245
+        to None to disable.
  246
+    :param fields_as: One of 'columns', 'rows'.
  247
+    :param row_cell_class: value that goes inside the <tr class="">
  248
+    :param cell_cell_class: value that goes inside the <td class="">
  249
+    :param header_cell_class: value that goes inside the <th class="">
  250
+
  251
+        "Fields as rows" returns one record per table in a table with two
  252
+        columns.
  253
+
  254
+        "Fields as columns" returns one record per table row in a table that
  255
+        has as many columns as field names, just like a regular spreadsheet.
  256
+    """
  257
+
  258
+    if fields_as == 'columns':
  259
+        return _HtmlColumnTable(app, validator, produce_header,
  260
+                                    table_name_attr, field_name_attr, border,
  261
+                                        row_class, cell_class, header_cell_class)
  262
+    elif fields_as == 'rows':
  263
+        return _HtmlRowTable(app, validator, produce_header,
  264
+                                 table_name_attr, field_name_attr, border,
  265
+                                        row_class, cell_class, header_cell_class)
  266
+
  267
+    else:
  268
+        raise ValueError(fields_as)
  269
+
  270
+class _HtmlTableBase(HtmlBase):
218 271
     mime_type = 'text/html'
219 272
 
220  
-    def __init__(self, app=None, validator=None, header_tag='th',
221  
-            table_name_attr='class', field_name_attr=None, border=0):
222  
-        """Protocol that returns the response object as a html table.
223  
-
224  
-        The simple flavour is like the HtmlMicroFormatprotocol, but returns data
225  
-        as a html table using the <table> tag.
226  
-
227  
-        :param app: A rpclib.application.Application instance.
228  
-        :param validator: The validator to use. Ignored.
229  
-        :param header_tag: The header tag used to show field names in the
230  
-            beginning of the table. Defaults to 'th'. Set to None to skip headers.
231  
-        :param table_name_attr: The name of the attribute that will contain the
232  
-            response name of the complex object in the table tag. Set to None to
233  
-            disable.
234  
-        :param field_name_attr: The name of the attribute that will contain the
235  
-            field names of the complex object children for every table cell. Set
236  
-            to None to disable.
237  
-        """
  273
+    def __init__(self, app, validator, produce_header, table_name_attr,
  274
+                 field_name_attr, border, row_class, cell_class, header_class):
238 275
 
239 276
         HtmlBase.__init__(self, app, validator)
240 277
 
241  
-        assert header_tag in ('td','th')
242  
-        assert table_name_attr in (None, 'class','id')
243  
-        assert field_name_attr in (None, 'class','id')
  278
+        assert table_name_attr in (None, 'class', 'id')
  279
+        assert field_name_attr in (None, 'class', 'id')
244 280
 
245  
-        self.__header_tag = header_tag
  281
+        self.__produce_header = produce_header
246 282
         self.__table_name_attr = table_name_attr
247 283
         self.__field_name_attr = field_name_attr
248 284
         self.__border = border
  285
+        self.row_class = row_class
  286
+        self.cell_class = cell_class
  287
+        self.header_class = header_class
  288
+
  289
+        if self.cell_class is not None and field_name_attr == 'class':
  290
+            raise Exception("Either 'cell_class' should be None or "
  291
+                            "field_name_attr should be != 'class'")
  292
+        if self.header_class is not None and field_name_attr == 'class':
  293
+            raise Exception("Either 'header_class' should be None or "
  294
+                            "field_name_attr should be != 'class'")
249 295
 
250 296
     @property
251 297
     def border(self):
252 298
         return self.__border
253 299
 
254 300
     @property
255  
-    def header_tag(self):
256  
-        return self.__header_tag
  301
+    def produce_header(self):
  302
+        return self.__produce_header
257 303
 
258 304
     @property
259 305
     def table_name_attr(self):
@@ -263,7 +309,7 @@ def table_name_attr(self):
263 309
     def field_name_attr(self):
264 310
         return self.__field_name_attr
265 311
 
266  
-    def serialize_impl(self, cls, inst):
  312
+    def serialize_impl(self, cls, inst, locale):
267 313
         name = cls.get_type_name()
268 314
 
269 315
         if self.table_name_attr is None:
@@ -271,7 +317,7 @@ def serialize_impl(self, cls, inst):
271 317
         else:
272 318
             out_body_doc_header = ['<table %s="%s">' % (self.table_name_attr, name)]
273 319
 
274  
-        out_body_doc = self.serialize_complex_model(cls, inst)
  320
+        out_body_doc = self.serialize_complex_model(cls, inst, locale)
275 321
 
276 322
         out_body_doc_footer = ['</table>']
277 323
 
@@ -281,7 +327,9 @@ def serialize_impl(self, cls, inst):
281 327
                 out_body_doc_footer,
282 328
             )
283 329
 
284  
-    def serialize_complex_model(self, cls, value):
  330
+
  331
+class _HtmlColumnTable(_HtmlTableBase):
  332
+    def serialize_complex_model(self, cls, value, locale):
285 333
         sti = None
286 334
         fti = cls.get_flat_type_info(cls)
287 335
 
@@ -289,50 +337,244 @@ def serialize_complex_model(self, cls, value):
289 337
         if len(fti) == 1:
290 338
             fti = first_child.get_flat_type_info(first_child)
291 339
             first_child = iter(fti.values()).next()
  340
+
292 341
             if len(fti) == 1 and first_child.Attributes.max_occurs > 1:
293 342
                 if issubclass(first_child, ComplexModelBase):
294 343
                     sti = first_child.get_simple_type_info(first_child)
295  
-                value = value[0]
  344
+
296 345
             else:
297  
-                raise Exception("Can only serialize Array(...) types")
  346
+                raise NotImplementedError("Can only serialize Array(...) types")
  347
+            
  348
+            value = value[0]
  349
+
298 350
         else:
299  
-            raise Exception("Can only serialize single Array(...) return types")
  351
+            raise NotImplementedError("Can only serialize single Array(...) return types")
  352
+
  353
+        tr = {}
  354
+        if self.row_class is not None:
  355
+            tr['class'] = self.row_class
  356
+
  357
+        td = {}
  358
+        if self.cell_class is not None:
  359
+            td['class'] = self.cell_class
300 360
 
301  
-        header_row = E.tr()
302 361
         class_name = first_child.get_type_name()
303  
-        if sti is None:
304  
-            header_row.append(E.th(class_name))
305  
-        else:
306  
-            if self.field_name_attr is None:
307  
-                for k, v in sti.items():
308  
-                    header_row.append(E.th(k))
  362
+        if self.produce_header:
  363
+            header_row = E.tr(**tr)
  364
+
  365
+            th = {}
  366
+            if self.header_class is not None:
  367
+                th['class'] = self.header_class
  368
+
  369
+            if sti is None:
  370
+                header_row.append(E.th(class_name, **th))
  371
+
309 372
             else:
310  
-                for k, v in sti.items():
311  
-                    header_row.append(E.th(k,
312  
-                                        **{self.field_name_attr: k}))
  373
+                if self.field_name_attr is not None:
  374
+                    for k, v in sti.items():
  375
+                        header_row.append(E.th(k), **th)
  376
+                else:
  377
+                    for k, v in sti.items():
  378
+                        th[self.field_name_attr] = k
  379
+                        header_name = translate(v, locale, k)
  380
+                        header_row.append(E.th(header_name), **th)
  381
+
  382
+
  383
+            yield header_row
313 384
 
314  
-        yield header_row
315 385
 
316 386
         if sti is None:
317 387
             if self.field_name_attr is None:
318 388
                 for val in value:
319  
-                    yield E.tr(E.td(first_child.to_string(val)), )
  389
+                    yield E.tr(E.td(first_child.to_string(val), ** td), ** tr)
320 390
             else:
321 391
                 for val in value:
322  
-                    yield E.tr(E.td(first_child.to_string(val)),
323  
-                                           **{self.field_name_attr: class_name})
  392
+                    td[self.field_name_attr] = class_name
  393
+                    yield E.tr(E.td(first_child.to_string(val), ** td), ** tr)
324 394
 
325 395
         else:
326 396
             for val in value:
327 397
                 row = E.tr()
328  
-                print val
329 398
                 for k, v in sti.items():
330 399
                     subvalue = val
331 400
                     for p in v.path:
332  
-                        subvalue = getattr(subvalue, p, "`%s`" % k)
  401
+                        subvalue = getattr(subvalue, p, None)
  402
+
  403
+                    if subvalue is None:
  404
+                        if v.type.Attributes.min_occurs == 0:
  405
+                            continue
  406
+                        else:
  407
+                            subvalue = ""
  408
+                    else:
  409
+                        subvalue = v.type.to_string(subvalue)
  410
+
  411
+
  412
+                    if self.field_name_attr is None:
  413
+                        row.append(E.td(subvalue, **td))
  414
+                    else:
  415
+                        td[self.field_name_attr] = k
  416
+                        row.append(E.td(subvalue, **td))
  417
+
  418
+                yield row
  419
+
  420
+
  421
+class _HtmlRowTable(_HtmlTableBase):
  422
+    def serialize_complex_model(self, cls, value, locale):
  423
+        sti = None
  424
+        fti = cls.get_flat_type_info(cls)
  425
+        is_array = False
  426
+
  427
+        first_child = iter(fti.values()).next()
  428
+        if len(fti) == 1:
  429
+            fti = first_child.get_flat_type_info(first_child)
  430
+            first_child_2 = iter(fti.values()).next()
  431
+
  432
+            if len(fti) == 1 and first_child_2.Attributes.max_occurs > 1:
  433
+                if issubclass(first_child_2, ComplexModelBase):
  434
+                    sti = first_child_2.get_simple_type_info(first_child_2)
  435
+                is_array = True
  436
+
  437
+            else:
  438
+                if issubclass(first_child, ComplexModelBase):
  439
+                    sti = first_child.get_simple_type_info(first_child)
  440
+            
  441
+            value = value[0]
  442
+
  443
+        else:
  444
+            raise NotImplementedError("Can only serialize single return types")
  445
+
  446
+        tr = {}
  447
+        if self.row_class is not None:
  448
+            tr['class'] = self.row_class
  449
+
  450
+        td = {}
  451
+        if self.cell_class is not None:
  452
+            td['class'] = self.cell_class
  453
+
  454
+        th = {}
  455
+        if self.header_class is not None:
  456
+            th['class'] = self.header_class
  457
+
  458
+        class_name = first_child.get_type_name()
  459
+        if sti is None:
  460
+            if self.field_name_attr is None:
  461
+                if is_array:
  462
+                    for val in value:
  463
+                        yield E.tr(E.td(first_child_2.to_string(val)),)
  464
+                else:
  465
+                    yield E.tr(E.td(first_child_2.to_string(value)),)
  466
+
  467
+            else:
  468
+                if self.field_name_attr is not None:
  469
+                    td[self.field_name_attr] = class_name
  470
+
  471
+                if is_array:
  472
+                    for val in value:
  473
+                        yield E.tr(E.td(first_child_2.to_string(val), **td), **tr)
  474
+                else:
  475
+                    yield E.tr(E.td(first_child_2.to_string(value), **td), **tr)
  476
+
  477
+        else:
  478
+            for k, v in sti.items():
  479
+                row = E.tr(**tr)
  480
+                subvalue = value
  481
+                for p in v.path:
  482
+                    subvalue = getattr(subvalue, p, None)
  483
+
  484
+                if subvalue is None:
  485
+                    if v.type.Attributes.min_occurs == 0:
  486
+                        continue
  487
+                    else:
  488
+                        subvalue = ""
  489
+                else:
  490
+                    subvalue = v.type.to_string(subvalue)
  491
+
  492
+                    if issubclass(v.type, ImageUri):
  493
+                        subvalue = E.img(src=subvalue)
  494
+
  495
+                if self.produce_header:
  496
+                    header_text = translate(v.type, locale, k)
333 497
                     if self.field_name_attr is None:
334  
-                        row.append(E.td(v.type.to_string(subvalue)))
  498
+                        row.append(E.th(header_text, **th))
335 499
                     else:
336  
-                        row.append(E.td(v.type.to_string(subvalue),
337  
-                                                   **{self.field_name_attr: k}))
  500
+                        th[self.field_name_attr] = k
  501
+                        row.append(E.th(header_text, **th))
  502
+
  503
+                if self.field_name_attr is None:
  504
+                    row.append(E.td(subvalue, **td))
  505
+
  506
+                else:
  507
+                    td[self.field_name_attr] = k
  508
+                    row.append(E.td(subvalue, **td))
  509
+
338 510
                 yield row
  511
+
  512
+
  513
+class _HtmlPage(object):
  514
+    """An EXPERIMENTAL protocol-ish that parses and generates a template for
  515
+    a html file.
  516
+
  517
+    >>> open('temp.html', 'w').write('<html><body><div id="some_div" /></body></html>')
  518
+    >>> t = _HtmlPage('temp.html')
  519
+    >>> t.some_div = "some_text"
  520
+    >>> from lxml import html
  521
+    >>> print html.tostring(t.html)
  522
+    <html><body><div id="some_div">some_text</div></body></html>
  523
+    """
  524
+
  525
+    def __init__(self, file_name):
  526
+        self.__frozen = False
  527
+        self.__file_name = file_name
  528
+        self.__html = html.fromstring(open(file_name, 'r').read())
  529
+
  530
+        self.__ids = {}
  531
+        for elt in self.__html.xpath('//*[@id]'):
  532
+            key = elt.attrib['id']
  533
+            if key in self.__ids:
  534
+                raise ValueError("Don't use duplicate values in id attributes in"
  535
+                                 "template documents.")
  536
+            self.__ids[key] = elt
  537
+            s = "%r -> %r" % (key, elt)
  538
+            logger.debug(s)
  539
+
  540
+        self.__frozen = True
  541
+
  542
+    @property
  543
+    def file_name(self):
  544
+        return self.__file_name
  545
+
  546
+    @property
  547
+    def html(self):
  548
+        return self.__html
  549
+
  550
+    def __getattr__(self, key):
  551
+        try:
  552
+            return object.__getattr__(self, key)
  553
+
  554
+        except AttributeError:
  555
+            try:
  556
+                return self.__ids[key]
  557
+            except KeyError:
  558
+                raise AttributeError(key)
  559
+
  560
+    def __setattr__(self, key, value):
  561
+        if key.endswith('__frozen') or not self.__frozen:
  562
+            object.__setattr__(self, key, value)
  563
+
  564
+        else:
  565
+            elt = self.__ids.get(key, None)
  566
+            if elt is None:
  567
+                raise AttributeError(key)
  568
+
  569
+            # poor man's elt.clear() version that keeps the attributes
  570
+            children = list(elt)
  571
+            for c in children:
  572
+                elt.remove(c)
  573
+            elt.text = None
  574
+            elt.tail = None
  575
+
  576
+            # set it in.
  577
+            if isinstance(value, basestring):
  578
+                elt.text = value
  579
+            else:
  580
+                elt.append(value)
2  src/rpclib/test/interface/test_bare_interface.py
@@ -78,7 +78,7 @@ def some_other_call(ctx, sth):
78 78
         imports = application.interface.imports
79 79
         tns = application.interface.get_tns()
80 80
         smm = application.interface.service_method_map
81  
-        print imports
  81
+        print(imports)
82 82
 
83 83
         assert imports[tns] == set(['1','3','4'])
84 84
         assert imports['3'] == set(['2'])
4  src/rpclib/test/interface/test_xml_schema.py
@@ -49,7 +49,7 @@ class SomeType(ComplexModel):
49 49
         from lxml import etree
50 50
 
51 51
         docs = get_schema_documents([SomeType])
52  
-        print etree.tostring(docs['tns'], pretty_print=True)
  52
+        print(etree.tostring(docs['tns'], pretty_print=True))
53 53
         any = docs['tns'].xpath('//xsd:any', namespaces={'xsd': ns.xsd})
54 54
 
55 55
         assert len(any) == 1
@@ -89,7 +89,7 @@ def some_call(ctx, sth):
89 89
         imports = application.interface.imports
90 90
         smm = application.interface.service_method_map
91 91
 
92  
-        print smm
  92
+        print(smm)
93 93
 
94 94
         raise NotImplementedError('test something!')
95 95
 
6  src/rpclib/test/interop/test_soap_client_http_twisted.py
@@ -41,7 +41,7 @@ def cb(ret):
41 41
 
42 42
     def test_python_exception(self):
43 43
         def eb(ret):
44  
-            print ret
  44
+            print(ret)
45 45
 
46 46
         def cb(ret):
47 47
             assert False, "must fail: %r" % ret
@@ -50,7 +50,7 @@ def cb(ret):
50 50
 
51 51
     def test_soap_exception(self):
52 52
         def eb(ret):
53  
-            print type(ret)
  53
+            print(type(ret))
54 54
 
55 55
         def cb(ret):
56 56
             assert False, "must fail: %r" % ret
@@ -59,7 +59,7 @@ def cb(ret):
59 59
 
60 60
     def test_documented_exception(self):
61 61
         def eb(ret):
62  
-            print ret
  62
+            print(ret)
63 63
 
64 64
         def cb(ret):
65 65
             assert False, "must fail: %r" % ret
2  src/rpclib/test/model/test_binary.py
@@ -35,7 +35,7 @@ def setUp(self):
35 35
     def test_data(self):
36 36
         element = etree.Element('test')
37 37
         Soap11().to_parent_element(ByteArray, self.data, ns_test, element)
38  
-        print etree.tostring(element, pretty_print=True)
  38
+        print(etree.tostring(element, pretty_print=True))
39 39
         element = element[0]
40 40
 
41 41
         a2 = Soap11().from_element(ByteArray, element)
2  src/rpclib/test/model/test_complex.py
@@ -342,7 +342,7 @@ class CM(ComplexModel):
342 342
         pref = CM.get_namespace_prefix(interface)
343 343
         type_def = wsdl.get_schema_info(pref).types[CM.get_type_name()]
344 344
         attribute_def = type_def.find('{%s}attribute' % xml_ns.xsd)
345  
-        print etree.tostring(type_def, pretty_print=True)
  345
+        print(etree.tostring(type_def, pretty_print=True))
346 346
 
347 347
         self.assertIsNotNone(attribute_def)
348 348
         self.assertEqual(attribute_def.get('name'), 'a')
6  src/rpclib/test/model/test_enum.py
@@ -81,7 +81,7 @@ def test_wsdl(self):
81 81
         elt = etree.fromstring(wsdl)
82 82
         simple_type = elt.xpath('//xs:simpleType', namespaces=self.app.interface.nsmap)[0]
83 83
 
84  
-        print(etree.tostring(elt, pretty_print=True))
  84
+        print((etree.tostring(elt, pretty_print=True)))
85 85
         print(simple_type)
86 86
 
87 87
         self.assertEquals(simple_type.attrib['name'], 'DaysOfWeekEnum')
@@ -131,7 +131,7 @@ def test_serialize_complex_array(self):
131 131
         ret = XmlObject().from_element(Array(DaysOfWeekEnum), elt)
132 132
         assert days == ret
133 133
 
134  
-        print(etree.tostring(elt, pretty_print=True))
  134
+        print((etree.tostring(elt, pretty_print=True)))
135 135
 
136 136
         pprint(self.app.interface.nsmap)
137 137
         assert days_xml == [ (e.tag, e.text) for e in
@@ -154,7 +154,7 @@ def test_serialize_simple_array(self):
154 154
         XmlObject().to_parent_element(Test, t, 'test_namespace', elt)
155 155
         elt = elt[0]
156 156
 
157  
-        print(etree.tostring(elt, pretty_print=True))
  157
+        print((etree.tostring(elt, pretty_print=True)))
158 158
 
159 159
         ret = XmlObject().from_element(Test, elt)
160 160
         self.assertEquals(t.days, ret.days)
2  src/rpclib/test/model/test_exception.py
@@ -180,7 +180,7 @@ def get_type_name_ns(self, app):
180 180
         self.assertEqual(len(interface.classes), 1)
181 181
         c_cls = interface.classes.values()[0]
182 182
         c_elt = schema.types.values()[0]
183  
-        print c_cls, cls
  183
+        print(c_cls, cls)
184 184
         self.failUnless(c_cls is cls)
185 185
         self.assertEqual(c_elt.tag, '{%s}complexType' % ns_xsd)
186 186
         self.assertEqual(c_elt.get('name'), 'Fault')
2  src/rpclib/test/model/test_primitive.py
@@ -253,7 +253,7 @@ def test_unicode(self):
253 253
     def test_null(self):
254 254
         element = etree.Element('test')
255 255
         XmlObject().to_parent_element(Null, None, ns_test, element)
256  
-        print etree.tostring(element)
  256
+        print(etree.tostring(element))
257 257
 
258 258
         element = element[0]
259 259
         self.assertTrue( bool(element.attrib.get('{%s}nil' % ns.xsi)) )
141  src/rpclib/test/protocol/test_html_table.py
@@ -22,6 +22,9 @@
22 22
 
23 23
 import unittest
24 24
 
  25
+from pprint import pformat
  26
+from urllib import urlencode
  27
+
25 28
 from lxml import html
26 29
 
27 30
 from rpclib.application import Application
@@ -37,18 +40,36 @@
37 40
 from rpclib.server.wsgi import WsgiMethodContext
38 41
 from rpclib.server.wsgi import WsgiApplication
39 42
 
  43
+def _start_response(code, headers):
  44
+    print(code, pformat(headers))
  45
+
  46
+def _call_wsgi_app_kwargs(app, **kwargs):
  47
+    return _call_wsgi_app(app, kwargs.items())
  48
+
  49
+def _call_wsgi_app(app, pairs):
  50
+    out_string = ''.join(app({
  51
+        'QUERY_STRING': urlencode(pairs),
  52
+        'PATH_INFO': '/some_call',
  53
+        'REQUEST_METHOD': 'GET',
  54
+        'SERVER_NAME': 'rpclib.test',
  55
+        'SERVER_PORT': '0',
  56
+        'wsgi.url_scheme': 'http',
  57
+    }, _start_response))
  58
+
  59
+    return out_string
40 60
 
41  
-class TestHtmlTable(unittest.TestCase):
42  
-    def test_complex_array(self):
43  
-        class CM(ComplexModel):
44  
-            i = Integer
45  
-            s = String
46 61
 
47  
-        class CCM(ComplexModel):
48  
-            c = CM
49  
-            i = Integer
50  
-            s = String
  62
+class CM(ComplexModel):
  63
+    i = Integer
  64
+    s = String
51 65
 
  66
+class CCM(ComplexModel):
  67
+    c = CM
  68
+    i = Integer
  69
+    s = String
  70
+
  71
+class TestHtmlColumnTable(unittest.TestCase):
  72
+    def test_complex_array(self):
52 73
         class SomeService(ServiceBase):
53 74
             @srpc(CCM, _returns=Array(CCM))
54 75
             def some_call(ccm):
@@ -57,26 +78,21 @@ def some_call(ccm):
57 78
         app = Application([SomeService], 'tns', HttpRpc(), HtmlTable(field_name_attr='class'), Wsdl11())
58 79
         server = WsgiApplication(app)
59 80
 
60  
-        initial_ctx = WsgiMethodContext(server, {
61  
-            'QUERY_STRING': 'ccm_c_s=abc&ccm_c_i=123&ccm_i=456&ccm_s=def',
62  
-            'PATH_INFO': '/some_call',
63  
-            'REQUEST_METHOD': 'GET',
64  
-        }, 'some-content-type')
  81
+        out_string = _call_wsgi_app_kwargs(server,
  82
+                ccm_i='456',
  83
+                ccm_s='def',
  84
+                ccm_c_i='123',
  85
+                ccm_c_s='abc',
  86
+            )
65 87
 
66  
-        ctx, = server.generate_contexts(initial_ctx)
67  
-        server.get_in_object(ctx)
68  
-        server.get_out_object(ctx)
69  
-        server.get_out_string(ctx)
70  
-
71  
-        out_string = ''.join(ctx.out_string)
72 88
         elt = html.fromstring(out_string)
73  
-        print html.tostring(elt, pretty_print=True)
  89
+        print(html.tostring(elt, pretty_print=True))
74 90
 
75 91
         resp = elt.find_class('some_callResponse')
76 92
         assert len(resp) == 1
77 93
         for i in range(len(elt)):
78 94
             row = elt[i]
79  
-            if i == 0:
  95
+            if i == 0:  # check for field names in table header
80 96
                 cell = row.findall('th[@class="i"]')
81 97
                 assert len(cell) == 1
82 98
                 assert cell[0].text == 'i'
@@ -94,7 +110,7 @@ def some_call(ccm):
94 110
                 assert cell[0].text == 's'
95 111
 
96 112
 
97  
-            else:
  113
+            else: # check for field values in table body
98 114
                 cell = row.findall('td[@class="i"]')
99 115
                 assert len(cell) == 1
100 116
                 assert cell[0].text == '456'
@@ -120,18 +136,77 @@ def some_call(s):
120 136
         app = Application([SomeService], 'tns', HttpRpc(), HtmlTable(), Wsdl11())
121 137
         server = WsgiApplication(app)
122 138
 
123  
-        initial_ctx = WsgiMethodContext(server, {
124  
-            'QUERY_STRING': 's=1&s=2',
125  
-            'PATH_INFO': '/some_call',
126  
-            'REQUEST_METHOD': 'GET',
127  
-        }, 'some-content-type')
  139
+        out_string = _call_wsgi_app(server, (('s', '1'), ('s', '2')) )
  140
+        assert out_string == '<table class="some_callResponse"><tr><th>string</th></tr><tr><td>1</td></tr><tr><td>2</td></tr></table>'
  141
+
  142
+class TestHtmlRowTable(unittest.TestCase):
  143
+    def test_complex(self):
  144
+        class SomeService(ServiceBase):
  145
+            @srpc(CCM, _returns=CCM)
  146
+            def some_call(ccm):
  147
+                return ccm
  148
+
  149
+
  150
+        app = Application([SomeService], 'tns', HttpRpc(),
  151
+                 HtmlTable(field_name_attr='class', fields_as='rows'), Wsdl11())
  152
+        server = WsgiApplication(app)
  153
+
  154
+        out_string = _call_wsgi_app_kwargs(server,
  155
+                         ccm_c_s='abc', ccm_c_i='123', ccm_i='456', ccm_s='def')
  156
+
  157
+        elt = html.fromstring(out_string)
  158
+        print(html.tostring(elt, pretty_print=True))
  159
+
  160
+        # Here's what this is supposed to return
  161
+        """
  162
+        <table class="some_callResponse">
  163
+            <tr>
  164
+                <th class="i">i</th>
  165
+                <td class="i">456</td>
  166
+            </tr>
  167
+            <tr>
  168
+                <th class="c_i">c_i</th>
  169
+                <td class="c_i">123</td>
  170
+            </tr>
  171
+            <tr>
  172
+                <th class="c_s">c_s</th>
  173
+                <td class="c_s">abc</td>
  174
+            </tr>
  175
+            <tr>
  176
+                <th class="s">s</th>
  177
+                <td class="s">def</td>
  178
+            </tr>
  179
+        </table>
  180
+        """
  181
+
  182
+        resp = elt.find_class('some_callResponse')
  183
+        assert len(resp) == 1
  184
+
  185
+        assert elt.xpath('//th[@class="i"]/text()')[0] == 'i'
  186
+        assert elt.xpath('//td[@class="i"]/text()')[0] == '456'
  187
+
  188
+        assert elt.xpath('//th[@class="c_i"]/text()')[0] == 'c_i'
  189
+        assert elt.xpath('//td[@class="c_i"]/text()')[0] == '123'
  190
+
  191
+        assert elt.xpath('//th[@class="c_s"]/text()')[0] == 'c_s'
  192
+        assert elt.xpath('//td[@class="c_s"]/text()')[0] == 'abc'
  193
+
  194
+        assert elt.xpath('//th[@class="s"]/text()')[0] == 's'
  195
+        assert elt.xpath('//td[@class="s"]/text()')[0] == 'def'
  196
+
  197
+
  198
+    def test_string_array(self):
  199
+        class SomeService(ServiceBase):
  200
+            @srpc(String(max_occurs='unbounded'), _returns=Array(String))
  201
+            def some_call(s):
  202
+                return s
  203
+
  204
+        app = Application([SomeService], 'tns', HttpRpc(), HtmlTable(fields_as='rows'), Wsdl11())
  205
+        server = WsgiApplication(app)
128 206
 
129  
-        ctx, = server.generate_contexts(initial_ctx)
130  
-        server.get_in_object(ctx)
131  
-        server.get_out_object(ctx)
132  
-        server.get_out_string(ctx)
  207
+        out_string = _call_wsgi_app(server, (('s', '1'), ('s', '2')) )
  208
+        assert out_string == '<table class="some_callResponse"><tr><td>1</td></tr><tr><td>2</td></tr></table>'
133 209
 
134  
-        assert ''.join(ctx.out_string) == '<table class="some_callResponse"><tr><th>string</th></tr><tr><td>1</td></tr><tr><td>2</td></tr></table>'
135 210
 
136 211
 if __name__ == '__main__':
137 212
     unittest.main()
2  src/rpclib/test/protocol/test_http.py
@@ -180,7 +180,7 @@ def some_call(ccm):
180 180
 
181 181
         server.get_out_string(ctx)
182 182
 
183  
-        print ctx.out_string
  183
+        print(ctx.out_string)
184 184
         assert ctx.out_string == ["CCM(i=1, c=CM(i=3, s='cs'), s='s')"]
185 185
 
186 186
     def test_nested_flatten_with_multiple_values_1(self):
24  src/rpclib/test/protocol/test_json.py
@@ -162,7 +162,7 @@ def some_call(ccm):
162 162
         server.get_out_string(ctx)
163 163
 
164 164
         ret = json.loads(''.join(ctx.out_string))
165  
-        print ret
  165
+        print(ret)
166 166
 
167 167
         assert ret['some_callResponse']['some_callResult']['i'] == 4
168 168
         assert ret['some_callResponse']['some_callResult']['s'] == '4x'
@@ -253,11 +253,11 @@ def some_call(ecm):
253 253
         ctx, = server.generate_contexts(initial_ctx)
254 254
         server.get_in_object(ctx)
255 255
         server.get_out_object(ctx)
256  
-        print ctx.in_object
  256
+        print(ctx.in_object)
257 257
         server.get_out_string(ctx)
258 258
 
259 259
         ret = json.loads(''.join(ctx.out_string))
260  
-        print ret
  260
+        print(ret)
261 261
         assert ret['some_callResponse']
262 262
         assert ret['some_callResponse']['some_callResult']
263 263
         assert ret['some_callResponse']['some_callResult']['ECM']
@@ -287,7 +287,7 @@ def test_invalid_request(self):
287 287
         class SomeService(ServiceBase):
288 288
             @srpc(Integer, String, DateTime)
289 289
             def yay(i,s,d):
290  
-                print i,s,d
  290
+                print(i,s,d)
291 291
                 pass
292 292
 
293 293
         app = Application([SomeService], 'tns', JsonObject(validator='soft'), JsonObject(), Wsdl11())
@@ -296,15 +296,15 @@ def yay(i,s,d):
296 296
         initial_ctx = MethodContext(server)
297 297
         initial_ctx.in_string = ['{"some_call": {"yay": []}}']
298 298
         ctx, = server.generate_contexts(initial_ctx)
299  
-        print ctx.in_error
  299
+        print(ctx.in_error)
300 300
         assert ctx.in_error.faultcode == 'Client.ResourceNotFound'
301  
-        print
  301
+        print()
302 302
 
303 303
     def test_invalid_string(self):
304 304
         class SomeService(ServiceBase):
305 305
             @srpc(Integer, String, DateTime)
306 306
             def yay(i,s,d):
307  
-                print i,s,d
  307
+                print(i,s,d)
308 308
                 pass
309 309
 
310 310
         app = Application([SomeService], 'tns', JsonObject(validator='soft'),
@@ -322,7 +322,7 @@ def test_invalid_number(self):
322 322
         class SomeService(ServiceBase):
323 323
             @srpc(Integer, String, DateTime)
324 324
             def yay(i,s,d):
325  
-                print i,s,d
  325
+                print(i,s,d)
326 326
                 pass
327 327
 
328 328
         app = Application([SomeService], 'tns', JsonObject(validator='soft'),
@@ -340,7 +340,7 @@ def test_missing_value(self):
340 340
         class SomeService(ServiceBase):
341 341
             @srpc(Integer, String, Mandatory.DateTime)
342 342
             def yay(i,s,d):
343  
-                print i,s,d
  343
+                print(i,s,d)
344 344
                 pass
345 345
 
346 346
         app = Application([SomeService], 'tns', JsonObject(validator='soft'),
@@ -352,7 +352,7 @@ def yay(i,s,d):
352 352
         ctx, = server.generate_contexts(initial_ctx)
353 353
         server.get_in_object(ctx)
354 354
 
355  
-        print ctx.in_error.faultstring
  355
+        print(ctx.in_error.faultstring)