Package turbogears :: Package widgets :: Module meta

Source Code for Module turbogears.widgets.meta

  1  """Metaclass for TurboGears widgets and support for external packages""" 
  2   
  3  __all__ = ['MetaWidget', 'load_template'] 
  4   
  5  import copy 
  6  import threading 
  7  import re 
  8   
  9  from inspect import isclass 
 10  from itertools import count, ifilter 
 11  from new import instancemethod 
 12   
 13  from turbogears import validators 
 14  from turbogears.util import setlike 
 15  from formencode.schema import Schema 
 16   
 17  default_engine = 'genshi' 
 18  param_prefix = '_param_' 
 19   
 20   
 21  ############################################################################# 
 22  # The meta class for all TurboGears widgets.                                # 
 23  ############################################################################# 
 24   
25 -class MetaWidget(type):
26 """The meta class for widgets.""" 27
28 - def __new__(mcs, name, bases, dct):
29 # Makes sure we get the union of params and member_widgets 30 # from all our bases. 31 params = setlike(dct.get('params', [])) 32 member_widgets = setlike(dct.get('member_widgets', [])) 33 compound = False 34 for base in bases: 35 params.add_all(getattr(base, 'params', [])) 36 if getattr(base, 'compound', False): 37 member_widgets.add_all(getattr(base, 'member_widgets', [])) 38 compound = True 39 for param in params: 40 # Swap all params listed at 'params' with a ParamDescriptor 41 try: 42 dct[param_prefix + param] = dct[param] 43 dct[param] = ParamDescriptor(param) 44 except KeyError: 45 for base in bases: 46 if hasattr(base, param): 47 break 48 else: 49 # not declared in any superclass, let default be None 50 dct[param_prefix + param] = None 51 dct[param] = ParamDescriptor(param) 52 params = list(params) 53 dct['params'] = params 54 if compound: 55 dct['member_widgets'] = list(member_widgets) 56 # Pick params_doc from all bases giving priority to the widget's own 57 params_doc = {} 58 for base in bases: 59 params_doc.update(getattr(base, 'params_doc', {})) 60 params_doc.update(dct.get('params_doc', {})) 61 dct['params_doc'] = params_doc 62 return super(MetaWidget, mcs).__new__(mcs, name, bases, dct)
63
64 - def __init__(mcs, name, bases, dct):
65 if "__init__" in dct: 66 dct['__init__'] = _decorate_widget_init(dct['__init__']) 67 mcs.__init__ = dct['__init__'] 68 super(MetaWidget, mcs).__init__(name, bases, dct) 69 if mcs.template: 70 mcs.template_c, mcs.template, mcs.engine_name = load_template( 71 mcs.template, mcs.engine_name) 72 mcs._locked = False
73 74 75 ############################################################################# 76 # Method decorators and other MetaWidget helpers # 77 ############################################################################# 78
79 -class ParamDescriptor(object):
80 """Descriptor to support automatic callable support for widget params.""" 81
82 - def __init__(self, param_name):
83 self.param_name = param_prefix + param_name
84
85 - def __get__(self, obj, typ=None):
86 if obj is None: 87 # return the original class attribute. This makes the descriptor 88 # almost invisible 89 return getattr(typ, self.param_name) 90 param = getattr(obj, self.param_name) 91 if callable(param): 92 return param() 93 return param
94
95 - def __set__(self, obj, value):
96 setattr(obj, self.param_name, value)
97 98
99 -def lockwidget(self, *args, **kw):
100 """Set this widget as locked the first time it's displayed.""" 101 gotlock = self._displaylock.acquire(False) 102 if gotlock: 103 del self.display 104 self._locked = True 105 output = self.__class__.display(self, *args, **kw) 106 if gotlock: 107 self._displaylock.release() 108 return output
109 110
111 -def _decorate_widget_init(func):
112 """Decorator for a widgets __init__ method. 113 114 Ensures that the display method for the instance is overridden by 115 lockwidget, that an eventual validator dict is applied to the widget 116 validator and that a validation schema is generated for compound widgets. 117 118 """ 119 120 def widget_init(self, *args, **kw): 121 self.display = instancemethod(lockwidget, self, self.__class__) 122 # We may want to move the InputWidget related logic to another 123 # decorator 124 input_widget = hasattr(self, "validator") 125 # Makes sure we only generate a schema once in the all __init__ 126 # cooperative calls from subclasses. Same for the displaylock. 127 if not hasattr(self, '__initstack'): 128 self._displaylock = threading.Lock() 129 self.__initstack = [] 130 else: 131 self.__initstack.append(True) 132 # Manage an eventual validator dictionary that provides additional 133 # parameters to the default validator (if present). 134 # We remove it from kw and apply it after the execution of the 135 # decorated __init__ method since only at this point we can check 136 # for the presence of a default validator 137 if (input_widget and ('validator' in kw) 138 and isinstance(kw['validator'], dict)): 139 validator_dict = kw.pop('validator') 140 else: 141 validator_dict = None 142 # Execute the decorated __init__ method 143 func(self, *args, **kw) 144 try: 145 self.__initstack.pop() 146 except IndexError: 147 # We're the first __init__ called 148 del self.__initstack 149 if input_widget: 150 # Apply an eventual validator dictionary 151 if validator_dict: 152 if self.validator is not None: 153 class_validator = self.__class__.validator 154 if self.validator is class_validator: 155 # avoid modifying a validator that has been 156 # defined at class level and therefor is 157 # shared by all instances of this widget 158 self.validator = copy.deepcopy(class_validator) 159 self.validator.__dict__.update(validator_dict) 160 else: 161 raise ValueError, ("You can't use a dictionary to " 162 "provide additional parameters " 163 "as the widget doesn't provide a " 164 "default validator" ) 165 # Generate the validation Schema if we are a compound widget 166 if getattr(self, 'compound', False): 167 widgets = self.iter_member_widgets() 168 # generate the schema associated to our widgets 169 validator = generate_schema(self.validator, widgets) 170 if getattr(self, 'repeating', False): 171 self.validator = validators.ForEach(validator) 172 else: 173 self.validator = validator
174 175 return widget_init 176 177 178 ############################################################################# 179 # Widget template support. # 180 ############################################################################# 181 182 _re_xmlns_py = re.compile( 183 r'^\s*(?:<\?xml*?\?>)?\s*(?:<!DOCTYPE.*?>)?(?:\s*<\?\w+.*?\?>)*' 184 r'\s*<\w+\b[^>]*?\bxmlns:py=["\'](.*?)["\']', re.DOTALL) 185
186 -def determine_template_engine(template):
187 """Determine the name of the engine from a template source.""" 188 engine_name = _re_xmlns_py.match(template) 189 if engine_name: 190 engine_name = {'http://genshi.edgewall.org/': 'genshi', 191 'http://purl.org/kid/ns#': 'kid'}.get(engine_name.group(1)) 192 return engine_name
193 194 195 _template_loader = {} 196
197 -def get_template_loader(engine_name):
198 """Get a template loader function for the given templating engine. 199 200 Raises an ImportError if the templating engine is not installed. 201 202 """ 203 try: 204 return _template_loader[engine_name] 205 except KeyError: 206 if engine_name == 'genshi': 207 from genshi.template import MarkupTemplate as GenshiMarkupTemplate 208 # For Genshi templates we can ignore the module name. 209 def load_template(template, mod_name=None): 210 """Load Genshi template""" 211 return GenshiMarkupTemplate(template)
212 elif engine_name == 'kid': 213 from kid import load_template as load_kid_template 214 # Create a counter for the widget instances which have overridden 215 # their template on the constructor in order to generate unique 216 # template module names. 217 instance_serial = count() 218 def load_template(template, mod_name=None): 219 """Load Kid template""" 220 if not mod_name: 221 mod_name = 'instance_template_%d' % instance_serial.next() 222 return load_kid_template(template, name=mod_name).Template 223 else: 224 return None # templating engine not supported 225 _template_loader[engine_name] = load_template 226 return load_template 227 228 229 _re_mod_name = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$") 230
231 -def load_template(template, engine_name=None, mod_name=None):
232 """Load the template with the engine into the module name. 233 234 If engine_name is None, it will be derived from the template. 235 If the engine cannot be derived, the default_engine will be used. 236 If mod_name is None, an unique one will be generated. 237 Returns a tuple (template_class, template_text, engine_name). 238 239 """ 240 241 if isinstance(template, basestring): 242 if template.startswith('genshi:') or template.startswith('kid:'): 243 engine_name, template = template.split(':', 1) 244 if _re_mod_name.match(template): # template path 245 template_c, template = template, None 246 if not engine_name: 247 engine_name = default_engine 248 else: # template source 249 if not engine_name: 250 engine_name = determine_template_engine( 251 template) or default_engine 252 try: 253 load_template = get_template_loader(engine_name) 254 except ImportError, error: 255 raise ImportError("%s\n\nCannot load the following template" 256 " because the %s engine is not installed:\n\n%s" 257 % (error, engine_name, template.strip())) 258 if load_template is None: 259 raise ValueError("TurboGears widgets do not support" 260 " %s templates." % engine_name) 261 template_c = load_template(template, mod_name) 262 else: # template class 263 if isinstance(template, MarkupTemplate): 264 engine_name = 'genshi' 265 elif isinstance(template, type): 266 engine_name = 'kid' 267 else: 268 engine_name = default_engine 269 template_c, template = template, None 270 return template_c, template, engine_name
271 272 273 ############################################################################# 274 # Schema generation support # 275 ############################################################################# 276
277 -class NullValidator(validators.FancyValidator):
278 """A do-nothing validator. 279 280 Used as a placeholder for fields with no validator so they don't get 281 stripped by the Schema. 282 283 """ 284 285 if_missing = None
286 287
288 -def copy_schema(schema):
289 """Recursively copy a schema.""" 290 new_schema = copy.copy(schema) 291 new_schema.pre_validators = schema.pre_validators[:] 292 new_schema.chained_validators = schema.chained_validators[:] 293 fields = {} 294 for k, v in schema.fields.iteritems(): 295 if isinstance(v, Schema): 296 v = copy_schema(v) 297 fields[k] = v 298 new_schema.fields = fields 299 return new_schema
300 301
302 -def merge_schemas(to_schema, from_schema, inplace=False):
303 """Recursively merge from_schema into to_schema 304 305 Takes care of leaving to_schema intact if inplace is False (default). 306 Returns a new Schema instance if inplace is False or to_schema is a Schema 307 class not an instance or the changed to_schema. 308 309 """ 310 # Nested schemas may be Schema (sub-)classes instead of instances. 311 # Copying Schema classes does not work, so we just create an instance. 312 if isclass(to_schema) and issubclass(to_schema, validators.Schema): 313 to_schema = to_schema() 314 elif not inplace: 315 to_schema = copy_schema(to_schema) 316 317 # Recursively merge child schemas 318 is_schema = lambda f: isinstance(f[1], validators.Schema) 319 seen = set() 320 for k, v in ifilter(is_schema, to_schema.fields.iteritems()): 321 seen.add(k) 322 from_field = from_schema.fields.get(k) 323 if from_field: 324 v = merge_schemas(v, from_field) 325 to_schema.add_field(k, v) 326 327 # Add remaining fields if we can 328 can_add = lambda f: f[0] not in seen and can_add_field(to_schema, f[0]) 329 for field in ifilter(can_add, from_schema.fields.iteritems()): 330 to_schema.add_field(*field) 331 332 return to_schema
333 334
335 -def add_field_to_schema(schema, widget):
336 """Add widget's validator if any to the given schema.""" 337 name = widget.name 338 if widget.validator is not None: 339 if isinstance(widget.validator, validators.Schema): 340 # Schema instance or class, might need to merge 'em... 341 if widget.name in schema.fields: 342 assert (isinstance(schema.fields[name], validators.Schema) 343 or issubclass(schema.fields[name], validators.Schema) 344 ), "Validator for '%s' should be a Schema subclass" % name 345 v = merge_schemas(schema.fields[name], widget.validator) 346 else: 347 v = widget.validator 348 schema.add_field(name, v) 349 elif can_add_field(schema, name): 350 # Non-schema validator, add it if we can... 351 schema.add_field(name, widget.validator) 352 elif can_add_field(schema, name): 353 schema.add_field(name, NullValidator())
354 355
356 -def generate_schema(schema, widgets):
357 """Generate or extend a copy of schema with all validators of all widgets. 358 359 schema may be a schema instance or class, widgets is a list of widgets 360 instances. 361 Returns the new schema instance. 362 363 """ 364 if schema is None: 365 schema = validators.Schema() 366 elif isclass(schema) and issubclass(schema, validators.Schema): 367 # allows to attach Schema (sub-)classes to widgets. 368 schema = schema() 369 else: 370 schema = copy_schema(schema) 371 for widget in widgets: 372 if widget.is_named: 373 add_field_to_schema(schema, widget) 374 return schema
375 376
377 -def can_add_field(schema, field_name):
378 """Checks if we can safely add a field. 379 380 Makes sure we're not overriding any field in the Schema. NullValidators 381 are ok to override. 382 383 """ 384 current_field = schema.fields.get(field_name) 385 return bool(current_field is None 386 or isinstance(current_field, NullValidator))
387