import re, weakref, logging from copy import copy from warnings import warn from itertools import ifilter, count, chain, izip, islice, ifilterfalse from inspect import isclass import tw from util import (assert_bool_attr, callable_wo_args, unflatten_args, OrderedSet, make_bunch, install_framework, ThreadSafeDescriptor, RequestLocalDescriptor, LRUCache) install_framework() import view from exceptions import * from meta import WidgetType, WidgetsList __all__ = [ "Widget", "WidgetsList", "WidgetRepeater", "RepeatedWidget", "WidgetBunch", "RepeatingWidgetBunch", "Child", "valid_id", "adapt_value", ] log = logging.getLogger(__name__) only_if_initialized = assert_bool_attr( '_is_initialized', True, WidgetUninitialized ) only_if_uninitialized = assert_bool_attr( '_is_initialized', False, WidgetInitialized ) def only_if_unlocked(func): def __setattr__(self, k, *args): descriptor = getattr(self.__class__, k, None) if not isinstance(descriptor, ThreadSafeDescriptor) and self._is_locked: raise WidgetLocked func(self, k, *args) return __setattr__ valid_id_re = re.compile(r'^[a-zA-Z][\w\-\_\:\.]*$') _deprecated_id_re = re.compile(r'^\w+$') def valid_id(s): if valid_id_re.match(s): return True elif s == '_method': #ignore this one small hack to help pages use HTTP verbs return True elif _deprecated_id_re.match(s): warn("The id %s will no longer be supported since it doesn't conform " "to the W3C Spec: http://www.w3.org/TR/xhtml1/#C_8" % s, DeprecationWarning, 3) return True else: return False serial_generator = count() class Widget(view.Renderable): """ Base class for all widgets. Example: .. sourcecode:: python >>> w = Widget('foo') >>> w.id 'foo' >>> w = Widget('foo', children=[Widget('c1'), Widget('c2')]) >>> [c.id for c in w.children] ['foo_c1', 'foo_c2'] >>> [c.parent.id for c in w.children] ['foo', 'foo'] It is a **must** that all initial state is entirely determined by the arguments to this function. This means that two widgets (of the same class) that receive the same parameters must behave in exactly the same way. You should not rely on external sources inside __init__ to set initial state. If you need to fetch data from external sources, do it at :meth:`update_params` instead. Essential pre, and post initialization is done in :meth:`__new__` and :meth:`post_init` respectively. :meth:`post_init` is guaranteed to run after the instance finishes initialization and it's behavior is rather special as all post_init's in mro will be called to have a chance to set final state in the instance. Basic pre-initialization consists of binding all kw arguments to the widget instance, attaching the widget to it's parent (if given), attaching the children and copying mutable arguments listed at :attr:`params` from the class to the instance to avoid accidental manipulation. .. sourcecode:: python >>> w = Widget('foo', a=1, b=2) >>> w.id 'foo' >>> w.a 1 >>> w.b 2 Basic post-initialization consists of caching required CSS and JS resources and setting the widget as initialized preventing further modification of it's attributes. .. sourcecode:: python >>> w = Widget('foo', a='1', b='2') >>> w.a = 'bar' Traceback (most recent call last): ... WidgetLocked: The widget is locked. It's unthread-safe to alter it's attributes after initialization. Widget attributes can only be modified in this method because widgets should behave in a state-less way as they are shared among threads for multiple requests. Per request modification of variables sent to the template should be done inside :meth:`update_params` and all state flowing from parent to children should occur inside that dict. Widgets should be instantiated at import time and reused among requests, most widgets allow overriding most of their parameters (not neccesarily all of them) at display time to change behavior. You should try avoiding instantiating widgets for every request as their initialization could be quite expensive for heavily nested widgets. Request-local storage provided by the hosting framework in ``tw.framework.request`` can be used to pass state among widgets which don't share the same root. """ __metaclass__ = WidgetType parent = None default = None params = { 'id': ('The id of this widget. This id is used to reference a widget ' 'from its parent ``children`` attribute and is usually the ' 'DOM id of outermost HTML tag of the widget.'), 'css_class': 'Main CSS class for this widget', 'css_classes': 'A list with extra css classes for the widget.' } css = [] javascript = [] css_classes = [] _is_initialized = False _is_locked = False def displays_on(self): if self.is_root: return tw.framework.default_view else: return self.parent.engine_name displays_on = property(displays_on, doc="""\ Where the widget is being displayed on """) #XXX: Some of these properties could be implemented as static attributes def id(self): return '_'.join(reversed( [w.id_path_elem for w in self.path if w.id_path_elem] )) or None id = property(id, doc="""\ The calculated id of the widget. This string will provide a unique id for each widget in the tree in a format which allows to re-recreate the nested structure. Example:: >>> A = Widget("A", children=[ ... Widget("B", children=[ ... Widget("C") ... ]) ... ]) ... >>> C = A.c.B.c.C >>> C.id 'A_B_C' """) def key(self): return '.' + '.'.join(reversed( [w.id_path_elem for w in self.path if w.id_path_elem][:-1] )) or None key = property(key, doc="""\ A string that can be used as a key to index the dictionary of parameters sent to the root widget so it reaches this widget when displaying. Example:: >>> A = Widget("A", children=[ ... Widget("B", children=[ ... Widget("C") ... ]) ... ]) ... >>> C = A.c.B.c.C >>> C.key '.B.C' """) def path(self): item = self while item: yield item item = item.parent path = property(path, doc="""\ Iterates a walk from this widget to the root of the tree """) @property def id_path_elem(self): return self._id def root(self): return list(self.path)[-1] root = property(root, doc="The root of this widget tree") def is_root(self): return self.parent is None is_root = property(is_root, doc="True if the widget doesn't have a parent") def __new__(cls, id=None, parent=None, children=[], **kw): """ Takes care of Widget instances creation. Should not need to be overrien likely. """ obj = view.Renderable.__new__(cls,id,parent,children,**kw) # The previous version of this used the __dict__ attribute to update # things, but that doesn't fire any fset properties, and can actually # lose data if a property is set, the following will not behave like # that for k,v in kw.iteritems(): if not k.startswith('_'): try: setattr(obj, k, v) except AttributeError, e: #skip setting the value of a read only property pass obj.orig_kw = kw.copy() if id is not None and not valid_id(id): raise ValueError("%r is not a valid id for a Widget"%id) obj._id = id obj._serial = serial_generator.next() # Attach the widget to its parent if parent is not None: if parent._is_initialized: raise WidgetInitialized obj.parent = weakref.proxy(parent) obj.parent.children.append(obj) # Append children passed as args or defined in the class, former # override later obj.c = obj.children = WidgetBunch() if isclass(children) and issubclass(children, WidgetsList): children = children() if not [obj._append_child(c) for c in children]: cls_children = cls._cls_children if isclass(cls_children) and issubclass(cls_children, WidgetsList): cls_children = cls_children() [obj._append_child(c) for c in cls_children] # Copy mutable attrs from __class__ into self, if not found in self # set to None for name in chain(cls.params, ['css', 'javascript']): try: attr = getattr(obj, name, None) if isinstance(attr, (list,dict)): attr = copy(attr) setattr(obj, name, attr) except AttributeError: # In case we try to set a read-only property pass # Initialize the static js calls list obj._js_calls = [] # Initialize resources OrderedSet obj._resources = OrderedSet() # Set default css class for the widget if not getattr(obj, 'css_class', None): obj.css_class = obj.__class__.__name__.lower() return obj @only_if_initialized def clone(self, *args, **kw): """ Returns a cloned version of the widget instance, optionally overriding initialization parameters. This is the only way to safely "modify" a widget instance. Example:: >>> w = Widget('foo', a=2) >>> w.id, w.a ('foo', 2) >>> w2 = w.clone(a=3) >>> w2.id, w2.a ('foo', 3) """ #log.debug("Cloning %r", self) return Child( self.__class__, self._id, children=self.children, **self.orig_kw )(*args, **kw) @only_if_initialized def _as_repeated(self, *args, **kw): cls = self.__class__ new_name = 'Repeated'+cls.__name__ new_class = type(new_name, (RepeatedWidget, cls), {}) #log.debug("Generating %r for repeating %r", new_class, self) return Child( new_class, self._id, children=self.children, **self.orig_kw )(*args, **kw) def __init__(self, id=None, parent=None, children=[], **kw): """Initializes a Widget instance. `id` The widget's id. All widgets in the same level of nested widgets trees should have distinct ids. `parent` A reference to the widget's parent. This parent needs to be in an uninitialized state which means it can only be passed to a child inside the parent's __init__ method. `children` A list, or WidgetsList (instance or class) or any other iterable with a reference to the children this widget should have. `\*\*kw` Any other extra keyword arguments for the widget. All of these will get bound to the Widget instance. """ # we need a dummy init here for the metaclass to wrap pass def _collect_resources(self): """picks up resources from self and all children""" oset = self._resources oset.add_all(chain(*[c._resources for c in self.css])) oset.add_all(chain(*[c._resources for c in self.javascript])) oset.add_all(chain(*[c._resources for c in self.children])) def post_init(self, *args, **kw): """ This method is called for all :class:`tw.api.Widget` base classes to perform final setup after the widget is initialized but before it is locked. """ if len(self._js_calls) > 0: from tw.core.resources import JSFunctionCalls #log.debug("Creating JSFunctionCalls for %r. Functions: %s", #self, self._js_calls, #) self.javascript.append( JSFunctionCalls(function_calls=self._js_calls) ) self._collect_resources() #log.debug("Finished initializing %r", self) assert not self._is_initialized self._is_initialized = True assert not self._is_locked self._is_locked = True def walk(self, filter=None, recur_if_filtered=True): """ Does a pre-order walk on widget tree rooted at self optionally applying a filter on them. Example:: >>> W = Widget >>> w = W('a', children=[W('b', children=[W('c')]), W('d')]) >>> ''.join(i._id for i in w.walk()) 'abcd' >>> ''.join(i._id for i in w.walk(lambda x: not x.is_root)) 'bcd' >>> ''.join(i._id for i in w.walk(lambda x: x._id == 'c')) 'c' Recursion can be prevented on children that not match filter. >>> ''.join(i._id for i in w.walk(lambda x: x._id == 'c', False)) '' """ def _walk(): yield self iterator = iter(self.children) if filter and not recur_if_filtered: iterator = self.ifilter_children(filter) for c in iterator: for w in c.walk(filter, recur_if_filtered): yield w if filter: return ifilter(filter, _walk()) return _walk() def add_call(self, call, location="bodybottom"): """ Adds a :func:`tw.api.js_function` call that will be made when the widget is rendered. """ if self._is_initialized: #log.debug("Adding call <%s> for %r dynamically.", call, self) from tw.core.resources import dynamic_js_calls dynamic_js_calls.inject_call(call, location) else: #log.debug("Adding call <%s> for %r statically.", call, self) self._js_calls.append(str(call)) @only_if_initialized def retrieve_css(self): warn("retrieve_css is deprecated. Please use retrieve_resources " "instead and filter them yourself", DeprecationWarning, 2) return [] @only_if_initialized def retrieve_javascript(self): warn("retrieve_javascript is deprecated. Please use retrieve_resources " "instead and filter them yourself", DeprecationWarning, 2) return [] @only_if_initialized def retrieve_resources(self): """ Returns a dict keyed by location with ordered collections of resources from this widget and its children as values. """ from tw.api import locations resources = dict((k, OrderedSet()) for k in locations) for r in self._resources: resources[r.location].add(r) return resources def adapt_value(self, value): """ Adapt object *value* for rendering in this widget. Should return one of: * A list of objects for repeated widgets. * A dict for widgets with children, keyed by the children's ids. * Any other object the widget understands. """ # Handle MultiDict instances if hasattr(value, 'dict_of_lists'): for k, v in value.dict_of_lists().items(): if len(v) == 1: value[k] = v[0] else: value[k] = v # If we have children, create a dict from the attributes of value elif len(self.children_deep) > 0 and not isinstance(value, (dict,list, tuple)): value = dict([ (w._id, getattr(value, w._id)) for w in self.children_deep if w._id and hasattr(value, w._id) ]) return value def register_resources(self): """ Register the resources required by this Widget with :attr:`tw.framework` for inclusion in the page. This method is called whenever a :class:`Widget` is rendered """ if self.is_root: tw.framework.register_resources(self.retrieve_resources()) def render(self, value=None, **kw): """ Renders a widget as an unicode string. """ kw = self.prepare_dict(value, kw) self.register_resources() return super(Widget, self).render(**kw) def display(self, value=None, **kw): """ Renders a widget and adapts the output. This method **must** be used to display child widgets inside their parent's template so output is adapted. Unlike :meth:`tw.api.Widget.render`, :meth:`tw.api.Widget.display` returns adapted output compatible with the template the widget is being rendered on. For example, this is needed so Genshi doesn't autoescape string output from mako and to serialize Genshi output on the other way around. """ kw = self.prepare_dict(value, kw) self.register_resources() return super(Widget, self).display(**kw) def __call__(self, value=None, **kw): return self.display(value, **kw) def get_default(self): """Returns the default value for the widget. If the default is a funtion that it can be called without arguments it will be called on each render to retrieve a value""" if callable_wo_args(self.default): value = self.default() else: value = self.default return value @property def children_deep(self): return self.children def prepare_dict(self, value, d, adapt=True): """ Prepares the all kw arguments sent to `display` or `render` before passing the kw argument's dict to `update_params`. """ if value is None: value = self.get_default() if adapt: d['value'] = self.adapt_value(value) else: d['value'] = value # Move args passed to child widgets into child_args child_args = d.setdefault('child_args', {}) for k in d.keys(): if '.' in k:# or '-' in k: child_args[k.lstrip('.')] = d.pop(k) d['args_for'] = self._get_child_args_getter(child_args) d['value_for'] = self._get_child_value_getter(d['value']) d['c'] = d['children'] = self.children d = make_bunch(d) self.update_params(d) s = set([d['css_class'],]) classes = d['css_classes'] for item in classes: s.add(item) # Compute the final css_class string d['css_class']= ' '.join(s) # reset the getters here so update_params has a chance to alter # the arguments to children and the value d['args_for'] = self._get_child_args_getter(d['child_args']) d['value_for'] = self._get_child_value_getter(d.get('value')) # Provide a shortcut to display a child field in the template d['display_child'] = self._child_displayer(self.children, d['value_for'], d['args_for']) return d def update_params(self, d): """ Updates the dict sent to the template for the current request. It is called when displaying or rendering a widget with all keyword arguments passed stuffed inside dict. Widget subclasses can call super cooperatively to avoid boiler-plate code as `Widget.update_params` takes care of pre-populating this dict with all attributes from self listed at `params` (copying them if mutable) and preparing arguments for child widgets. Any parameter sent to `display` or `render` will override those fetched from the instance or the class. Any function listed at `params` which can be called without arguments will be automatically called to fetch fresh results on every request. Parameters not found either on the class, the instance or the keyword args to `display` or `render` will be set to None. .. sourcecode:: python >>> class MyWidget(Widget): ... params = ["foo", "bar", "null"] ... foo = "foo" ... >>> w = MyWidget('test', bar=lambda: "bar") >>> d = {} >>> w.update_params(d) >>> d['bar'] 'bar' >>> d['foo'] 'foo' >>> d['null'] is None True >>> d = {'foo':'overriden'} >>> w.update_params(d) >>> d['foo'] 'overriden' """ # Populate dict with attrs from self listed at params for k in ifilterfalse(d.__contains__, self.params): attr = getattr(self, k, None) if attr is not None: if isinstance(attr, (list, dict)): attr = copy(attr) # Variables that are callable with no args are automatically # called here elif not isinstance(attr, Widget) and callable_wo_args(attr): log.debug("Autocalling param '%s'", k) attr = attr() d[k] = attr def ifilter_children(self, filter): """ Returns an iterator for all children applying a filter to them. .. sourcecode:: python >>> class Widgets(WidgetsList): ... aa = Widget() ... ab = Widget() ... ba = Widget() ... bb = Widget() ... >>> w = Widget(children=Widgets) >>> [c.id for c in w.ifilter_children(lambda w: w.id.startswith('a'))] ['aa', 'ab'] """ return ifilter(filter, self.children) def _get_child_value_getter(self, value): def value_getter(child_id): if value: if (hasattr(child_id, 'repetition') and isinstance(value,list) ): child_id = child_id.repetition elif isinstance(child_id, Widget) and isinstance(value,dict): child_id = child_id._id try: return value[child_id] except (IndexError,KeyError,TypeError): None return value_getter def _get_child_args_getter(self, child_args): if isinstance(child_args, dict): child_args = unflatten_args(child_args) def args_getter(child_id): if (hasattr(child_id, 'repetition') and isinstance(child_args, list) ): child_id = child_id.repetition elif (isinstance(child_id, Widget) and isinstance(child_args, dict) ): child_id = child_id._id try: return child_args[child_id] except (IndexError,KeyError,TypeError): return {} return args_getter @only_if_uninitialized def _append_child(self,obj): """Append an object as a child""" if isinstance(obj, Widget): obj._append_to(self) elif isinstance(obj, Child): obj(self) else: raise ValueError("Can only append Widgets or Childs, not %r" % obj) @only_if_initialized def _append_to(self, parent=None): return self.clone(parent) @only_if_unlocked def __setattr__(self,k,v): object.__setattr__(self,k,v) @only_if_unlocked def __delattr__(self,k): object.__delattr__(self,k) def __repr__(self): name = self.__class__.__name__ return "%s(%r, children=%r, **%r)" % ( name, self._id, self.children, self.orig_kw ) def __str__(self): return self.render() def __ne__(self, other): return not (self == other) def __eq__(self, other): return ( (getattr(other, '__class__', None) is self.__class__) and # Check _id so ancestors are not taken into account (other._id == self._id) and (other.children == self.children) and (other.orig_kw == self.orig_kw) ) @staticmethod def _child_displayer(children, value_for, args_for): def display_child(widget, **kw): if isinstance(widget, (basestring,int)): widget = children[widget] child_kw = args_for(widget) child_kw.update(kw) return widget.display(value_for(widget), **child_kw) return display_child # Available as a public symbol for easier extension with PEAK-Rules for # backwards compatibility adapt_value = Widget.adapt_value.im_func class Child(object): """ Prepares a Widget to being attached to a parent Widget. Creates a Widget instance with supplied arguments to the constructor when called (optionally overriding default arguments). >>> c = Child(Widget, 'foo') >>> w = c() >>> w.id 'foo' Parameters can be overriden when called. >>> w = c(id='bar') >>> w.id 'bar' """ __slots__ = ("widget_class", "id", "children", "kw") def __init__(self, widget_class, id=None, children=[], **kw): self.widget_class, self.id = widget_class, id, self.children, self.kw = children, kw def __call__(self, parent=None, **kw): kw_ = self.kw.copy() kw_.update(id=self.id, parent=parent, children=self.children) kw_.update(kw) return self.widget_class(**kw_) #XXX: Should enhance and clean up so setting widgets is not allowed if the # WidgetBunch is used as a widget's 'children' attribute and the widget # is already initialized. BTW, this WidgetBunch_attrs thingie is becomming # ugly ... should re-implement properly someday... _WidgetBunch_attrs = frozenset(['_widget_lst', '_widget_dct', '_widget', '_repetitions', '_parent', '_repetition_cache']) class WidgetBunch(object): """ An ordered collection of widgets. >>> from tw.core import Widget >>> wb = WidgetBunch(Widget('foo'), Widget('bar')) >>> wb[0].id == 'foo' True >>> wb[1].id == 'bar' True Exposes a mixed dict/list interface which permits indexing both by widget id and position. >>> wb['foo'] == wb[0] True >>> wb['bar'] == wb[1] True Also permits attribute access as long as the attribute doesn't conflict with an internal attribute, in case of conflict the internal attrbute will prevail. >>> wb.foo.id == 'foo' True >>> wb.append(Widget('append')) >>> wb.append.id Traceback (most recent call last): ... AttributeError: 'function' object has no attribute 'id' >>> wb['append'].id == 'append' True Iteration is also supported >>> [w.id for w in wb] ['foo', 'bar', 'append'] Some dict-like iterators too >>> [id for id in wb.iterkeys()] ['foo', 'bar', 'append'] >>> [id for id in wb.keys()] ['foo', 'bar', 'append'] >>> [w.id for w in wb.itervalues()] ['foo', 'bar', 'append'] >>> [w.id for w in wb.values()] ['foo', 'bar', 'append'] """ def __init__(self, *args): self._widget_lst = [] self._widget_dct = {} for wid in args: self.append(wid) def append(self,wid): self.__setattr__(wid._id, wid) def retrieve_css(self): oset = OrderedSet() for child in self: oset.add_all(child.retrieve_css()) return oset def retrieve_javascript(self): oset = OrderedSet() for child in self: oset.add_all(child.retrieve_javascript()) return oset def retrieve_resources(self): from tw.core.resources import merge_resources, locations resources = dict((k, OrderedSet()) for k in locations) for w in self: merge_resources(resources, w.retrieve_resources()) return resources def __getitem__(self, item): if isinstance(item, basestring): return self._widget_dct[item] elif isinstance(item, int): return self._widget_lst[item] raise KeyError, "No widget by %r" % item def __getattr__(self, name): if name in _WidgetBunch_attrs: return object.__getattribute__(self,name) try: return self[name] except KeyError: raise AttributeError, "No widget by %r" % name def __setattr__(self, name, value): # Note that this setattr allows name = None so append can use it. if name in _WidgetBunch_attrs: return object.__setattr__(self,name,value) self._widget_lst.append(value) self._widget_dct[name] = value def __iter__(self): for wid in self._widget_lst: yield wid def __ne__(self, other): return not (self==other) def __eq__(self, other): try: if len(self) == len(other): for a,b in izip(other,self): if a!=b: return False return True except TypeError: pass return False def __getslice__(self,start=0,stop=-1,step=1): return islice(self,start,stop,step) def __len__(self): return len(self._widget_lst) def __nonzero__(self): return bool(len(self)) def __contains__(self,item): dct = self._widget_dct try: return dct[item._id] == item except KeyError: return False def keys(self): return list(self.iterkeys()) def iterkeys(self): for w in self: yield w._id def values(self): return list(self.itervalues()) itervalues = __iter__ def __repr__(self): return `self._widget_lst` class RepeatingWidgetBunch(WidgetBunch): _repetitions = 0 _widget = None def __init__(self, *args, **kw): super(RepeatingWidgetBunch, self).__init__(*args, **kw) self._repetition_cache = LRUCache(100) def __len__(self): return self._repetitions def __iter__(self, reps=None): if reps is None: reps = self._repetitions for i in xrange(reps): yield self[i] def __getitem__(self, item): if not isinstance(item, int): raise KeyError("Must specify an integer") try: rep = self._repetition_cache[item] except KeyError: rep = self._widget.clone(repetition=item) # Work around widget's locking mechanism since it is thread-safe # to do it *here*. Note that the parent is not directly aware that # it has this repeated widget as a child and it doesn't care # really since I create those children on the fly object.__setattr__(rep, 'parent', self._parent) self._repetition_cache[item] = rep return rep class WidgetRepeater(Widget): params = ["repetitions", "extra"] widget = None repetitions = 3 max_repetitions = None extra = 0 id_path_elem = None template = "$output" @property def key(self): raise AttributeError("A WidgetRepeater has no meaningful key") def __new__(cls, id=None, parent=None, children=[], **kw): widget = kw.get('widget', getattr(cls, 'widget', None)) if widget is None: warn("No repeated widget specified in %s" % cls.__name__) widget = Widget() if isclass(widget) and issubclass(widget, Widget): widget = widget() obj = super(WidgetRepeater, cls).__new__(cls, id,parent,**kw) obj.c = obj.children = RepeatingWidgetBunch() obj.c._parent = weakref.proxy(obj) obj.c._repetitions = kw.get('repetitions', cls.repetitions) obj.c._widget = widget._as_repeated(obj, id=obj._id) return obj def update_params(self, d): super(WidgetRepeater,self).update_params(d) if d.get('max_repetitions', self.max_repetitions) is not None: warn("max_repetitions is deperecated and no longer has any effect", DeprecationWarning, 3) v_f = d['value_for'] a_f = d['args_for'] outputs = [ w.render(v_f(w), isextra=(w.repetition >= len(d['value'])), **a_f(w)) for w in d['children'].__iter__( max(d['repetitions'], len(d['value']) + d.extra)) ] d["output"] = '\n'.join(o for o in outputs if o) class RepeatedWidget(Widget): params = ["repetition"] repetition = 0 @property def id_path_elem(self): return '%s-%d' % (self._id, self.repetition or 0) @only_if_initialized def _as_repeated(self, *args, **kw): return self.clone(*args, **kw) if __name__ == "__main__": import doctest doctest.testmod(optionflags = doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE)