- Revision
- 11437
- Author
- pje
- Date
- 2006-08-17 12:18:11 -0700 (Thu, 17 Aug 2006)
Log Message
Replace 'afterChange' with '@schema.observer' decorator; see docs for details.
Modified Paths
- trunk/chandler/application/parcel-schema-guide.txt
- trunk/chandler/application/schema.py
- trunk/chandler/application/schema_api.txt
- trunk/chandler/parcels/osaf/app/__init__.py
- trunk/chandler/parcels/osaf/framework/scripting/script.py
- trunk/chandler/parcels/osaf/pim/calendar/Calendar.py
- trunk/chandler/parcels/osaf/pim/calendar/Recurrence.py
- trunk/chandler/parcels/osaf/pim/calendar/TimeZone.py
- trunk/chandler/projects/Chandler-PhotoPlugin/photos/Photos.py
Diff
Modified: trunk/chandler/application/parcel-schema-guide.txt (11436 => 11437)
--- trunk/chandler/application/parcel-schema-guide.txt 2006-08-17 18:54:38 UTC (rev 11436) +++ trunk/chandler/application/parcel-schema-guide.txt 2006-08-17 19:18:11 UTC (rev 11437) @@ -340,6 +340,82 @@ for more details. +Observers and Change Notifications +================================== + +Sometimes, you need to know when attributes have changed, in order to update +dependent attributes. The schema API allows you to register "observers": +methods that will be called when one or more specified attributes have changed. +For example:: + + >>> class Example(schema.Item): + ... foo = schema.One(schema.Integer) + ... bar = schema.One(schema.Text) + ... @schema.observer(foo, bar) + ... def something_changed(self, name): + ... print name, "changed" + +Whenever the ``foo`` or ``bar`` attribute of any ``Example`` instance is +changed, the instance's ``something_changed()`` method will be called, passing +in the name of the changed attribute:: + + >>> e = Example(itsView = rv) + >>> e.foo = 1 + foo changed + + >>> e.bar = u"Test" + bar changed + +Observer methods can be overridden by methods of the same name in a subclass:: + + >>> class Example2(Example): + ... def something_changed(self, name): + ... print name, "changed in Example2" + >>> e2 = Example2(itsView = rv) + >>> e2.foo = 1 + foo changed in Example2 + +Notice that this happens even if you do not explicitly register the subclass' +method as an observer. + +If you need to observe an attribute that is not defined in the current class, +you can do so by obtaining it from the base class, e.g.:: + + >>> class Example3(Example): + ... @schema.observer(Example.foo) + ... def foo_changed(self, name): + ... print name, "changed in Example3" + >>> e3 = Example3(itsView = rv) + >>> e3.foo = 1 + foo changed + foo changed in Example3 + >>> e3.bar = u"Test" + bar changed + +In the example above, the ``Example.something_changed()`` method was called, +*as well as* the ``Example3.foo_changed()`` method, because they have different +method names. Notice also that changing the ``bar`` attribute still called the +``something_changed`` method. + + +Important Limitations +--------------------- + +Please note that the order of observer calls is not guaranteed -- even +different installations of the same version of Chandler may have a different +callback order! Therefore, you must **always** write observer code so that it +does not depend on the order in which other observers are invoked. + +Also, because callbacks are registered by method name, you should take care not +to use the same method name for different things, unless you intend to override +an existing method. For the same reason, you should also give your observers +names that are unlikely to be accidentally duplicated in other classes. +Private method names -- that is, names beginning with a double-underscore +(``__``) can be good for this purpose, although they cannot be overridden in a +subclass unless you manually duplicate the name mangling that Python does +when you use private names. + + Extending Existing Kinds ========================
Modified: trunk/chandler/application/schema.py (11436 => 11437)
--- trunk/chandler/application/schema.py 2006-08-17 18:54:38 UTC (rev 11436) +++ trunk/chandler/application/schema.py 2006-08-17 19:18:11 UTC (rev 11437) @@ -21,6 +21,7 @@ from repository.schema import Types from repository.schema.Cloud import Cloud as _Cloud from repository.schema.Cloud import Endpoint as _Endpoint +from zope.interface.advice import getFrameInfo, addClassAdvisor import __main__, repository, threading, os, sys __all__ = [ @@ -29,7 +30,7 @@ 'importString', 'parcel_for_module', 'TypeReference', 'Enumeration', 'Cloud', 'Endpoint', 'addClouds', 'Struct', 'assertResolved', 'Annotation', 'AnnotationItem', - 'afterChange', + 'observer', ] all_aspects = Attribute.valueAspects + Attribute.refAspects + ('description',) @@ -538,18 +539,21 @@ if ai not in kind.attributes: kind.attributes.append(ai,name) - for attrName, names in cls.__dict__.get('__after_change__',{}).items(): - attr = kind.getAttribute(attrName, True) - if attr is not None: - if hasattr(attr, 'afterChange'): - afterChange = attr.afterChange - for name in names: + for name, attrNames in cls.__dict__.get('__after_change__',{}).items(): + for attrName in attrNames: + if isinstance(attrName,Descriptor): + attr = itemFor(attrName, view) + else: + attr = kind.getAttribute(attrName, True) + if attr is not None: + if hasattr(attr, 'afterChange'): + afterChange = attr.afterChange if name not in afterChange: afterChange.append(name) + else: + attr.afterChange = [name] else: - attr.afterChange = names - else: - view.logger.warn("no attribute '%s' defined for kind for %s", + view.logger.warn("no attribute '%s' defined for kind for %s", attrName, cls) def fixup(): @@ -941,9 +945,9 @@ __metaclass__ = EnumerationClass -def _update_info(name,attr,data): - from zope.interface.advice import getFrameInfo, addClassAdvisor - kind, module, _locals, _globals = getFrameInfo(sys._getframe(2)) +def _update_info(name,attr,data,frame=None,depth=2): + frame = frame or sys._getframe(depth) + kind, module, _locals, _globals = getFrameInfo(frame) if kind=='exec': # Fix for class-in-doctest-exec @@ -983,6 +987,7 @@ """ _update_info('kindInfo','__kind_info__',attrs) + @addClassAdvisor def callback(cls): _get_nrv() for k,v in attrs.items(): @@ -993,10 +998,7 @@ ) return cls - from zope.interface.advice import addClassAdvisor - addClassAdvisor(callback) - def addClouds(**clouds): """Declare clouds for a class' Kind @@ -1018,11 +1020,31 @@ _update_info('addClouds','__kind_clouds__',clouds) -def afterChange(**pairs): - """Update afterChange aspect on attributes""" - _update_info('afterChange', '__after_change__', pairs) +def observer(*attrs): + """Decorator to create an observer method for `attrs`""" + attrs = list(attrs) + for attr in attrs: + if not isinstance(attr, Descriptor): + raise TypeError( + repr(attr)+ + " is not a schema.Descriptor (One, Many, Sequence, etc.)" + ) + def decorator(func): + _update_info('observer', '__after_change__', {func.__name__:attrs}) + @addClassAdvisor + def callback(cls): + for attr in attrs: + if attr.owner is None or not issubclass(cls,attr.owner): + raise TypeError( + "%r does not belong to %r or its superclasses" + % (attr, cls) + ) + return cls + return func + return decorator + def importString(name, globalDict=__main__.__dict__): """Import an item specified by a string
Modified: trunk/chandler/application/schema_api.txt (11436 => 11437)
--- trunk/chandler/application/schema_api.txt 2006-08-17 18:54:38 UTC (rev 11436) +++ trunk/chandler/application/schema_api.txt 2006-08-17 19:18:11 UTC (rev 11437) @@ -269,6 +269,46 @@ TypeError: Descriptor objects are immutable; can't change 'doc'... once set +Change Notifications +==================== + +Test error messages: + + >>> class Example(schema.Item): + ... foo = schema.One(schema.Integer) + ... @schema.observer(foo) + ... def foo_changed(self, name): + ... print name, "changed" + >>> rv = NullRepositoryView(verify=True) + >>> e = Example(itsView = rv) + >>> e.foo = 1 + foo changed + + >>> class Example2(schema.Item): + ... @schema.observer(Example.foo) + ... def foo_changed(self, name): + ... print name, "changed" + Traceback (most recent call last): + ... + TypeError: ...foo... does not belong to ...Example2'> or its superclasses + + >>> class Example2(Example): + ... @schema.observer(Example.foo) + ... def foo_changed(self, name): + ... print name, "changed in Example2" + >>> e2 = Example2(itsView = rv) + >>> e2.foo = 1 + foo changed in Example2 + + >>> class BadExample(schema.Item): + ... @schema.observer("foo") + ... def foo_changed(self, name): + ... print name, "changed" + Traceback (most recent call last): + ... + TypeError: 'foo' is not a schema.Descriptor (One, Many, Sequence, etc.) + + --------------- Item Subclasses ---------------
Modified: trunk/chandler/parcels/osaf/app/__init__.py (11436 => 11437)
--- trunk/chandler/parcels/osaf/app/__init__.py 2006-08-17 18:54:38 UTC (rev 11436) +++ trunk/chandler/parcels/osaf/app/__init__.py 2006-08-17 19:18:11 UTC (rev 11437) @@ -26,16 +26,13 @@ import version class TZPrefs(Preferences): - showUI = schema.One(schema.Boolean, - initialValue = False, - afterChange = ['onShowUIChanged']) - - def onShowUIChanged(self, attrName): + showUI = schema.One(schema.Boolean, initialValue = False) + @schema.observer(showUI) + def onShowUIChanged(self, attrName): from osaf.pim.calendar.TimeZone import TimeZoneInfo - timeZoneInfo = TimeZoneInfo.get(self.itsView) - + # Sync up the default timezone (i.e. the one used when # creating new events). if self.showUI:
Modified: trunk/chandler/parcels/osaf/framework/scripting/script.py (11436 => 11437)
--- trunk/chandler/parcels/osaf/framework/scripting/script.py 2006-08-17 18:54:38 UTC (rev 11436) +++ trunk/chandler/parcels/osaf/framework/scripting/script.py 2006-08-17 19:18:11 UTC (rev 11437) @@ -52,8 +52,6 @@ who = schema.One(redirectTo = 'creator') date = schema.One(redirectTo = 'lastRan') - # afterChange - schema.afterChange(body = ['onBodyChanged']) def __init__(self, itsName=None, itsParent=None, itsKind=None, itsView=None, body=None, *args, **keys): @@ -88,6 +86,7 @@ self.body = newValue self._change_quietly = oldQuiet + @schema.observer(pim.ContentItem.body) def onBodyChanged(self, name): self.model_data_changed() @@ -195,7 +194,7 @@ for attr in __all__: builtIns[attr] = globals()[attr] - # Protect against scripts that don't stop, needed especially by + # Protect against scripts that don't stop, needed especially by # automated tests. scriptTimeout = int(getattr(Globals.options, 'scriptTimeout', 0)) if scriptTimeout > 0: @@ -273,7 +272,7 @@ testMask = Globals.options.chandlerTestMask from tools.cats.framework.runTests import run_perf_tests run_perf_tests(chandlerPerformanceTests, debug=testDebug, mask=testMask, logName=logFileName) - + fileName = Globals.options.scriptFile if fileName: scriptFileText = script_file(fileName) @@ -284,7 +283,7 @@ global_cats_profiler = profiler profiler.runcall(run_script_with_symbols, scriptFileText, - fileName=fileName, + fileName=fileName, profiler=profiler, builtIns=builtIns) profiler.close() @@ -292,7 +291,7 @@ else: run_script_with_symbols(scriptFileText, fileName = fileName, builtIns=builtIns) - + global_cats_profiler = None # remember the CATS profiler here def cats_profiler():
Modified: trunk/chandler/parcels/osaf/pim/calendar/Calendar.py (11436 => 11437)
--- trunk/chandler/parcels/osaf/pim/calendar/Calendar.py 2006-08-17 18:54:38 UTC (rev 11436) +++ trunk/chandler/parcels/osaf/pim/calendar/Calendar.py 2006-08-17 19:18:11 UTC (rev 11437) @@ -1355,22 +1355,11 @@ else: RecurrenceDialog.getProxy(u'ui', self).removeFromCollection(collection, cutting) - changeNames = ('displayName', 'startTime', 'duration', 'location', 'body', - 'lastModified', 'allDay') - # KLUDGE: - # replace the following with the proper schema API syntax - # and possible per-attribute refactoring - - schema.afterChange(displayName=['onEventChanged'], - startTime=['onEventChanged'], - duration=['onEventChanged'], - location=['onEventChanged'], - body=['onEventChanged'], - lastModified=['onEventChanged'], - allDay=['onEventChanged'], - rruleset=['onEventChanged']) - + @schema.observer( + ContentItem.displayName, ContentItem.body, ContentItem.lastModified, + startTime, duration, location, allDay, rruleset + ) def onEventChanged(self, name): """ Maintain coherence of the various recurring items associated with self @@ -1379,10 +1368,9 @@ """ # allow initialization code to avoid triggering onEventChanged rruleset = name == 'rruleset' - changeName = not rruleset and name in CalendarEventMixin.changeNames + changeName = not rruleset - if (not (rruleset or changeName) or - self.rruleset is None or + if (self.rruleset is None or getattr(self, '_share_importing', False) or getattr(self, '_ignoreValueChanges', False)): return
Modified: trunk/chandler/parcels/osaf/pim/calendar/Recurrence.py (11436 => 11437)
--- trunk/chandler/parcels/osaf/pim/calendar/Recurrence.py 2006-08-17 18:54:38 UTC (rev 11436) +++ trunk/chandler/parcels/osaf/pim/calendar/Recurrence.py 2006-08-17 19:18:11 UTC (rev 11437) @@ -206,28 +206,17 @@ notSpecialNames = ("interval", "until", "bysetpos", "bymonth", "bymonthday", "byyearday","byweekno", "byhour", "byminute", "bysecond") - allNames = ("interval", "until", "bysetpos", "bymonth", "bymonthday", - "byyearday","byweekno", "byhour", "byminute", "bysecond", - "wkst", "byweekday", "freq") + @schema.observer( + interval, until, bysetpos, bymonth, bymonthday, byyearday, byweekno, + byhour, byminute, bysecond, wkst, byweekday, freq + ) + def onRecurrenceChanged(self, name): + """If the rule changes, update any associated events.""" + for ruletype in ('rruleFor', 'exruleFor'): + if self.hasLocalAttributeValue(ruletype): + getattr(self, ruletype).onRuleSetChanged('rrules') - # KLUDGE: - # replace the following with the proper schema API syntax - # and possible per-attribute refactoring - schema.afterChange(interval = ['onRecurrenceChanged'], - until = ['onRecurrenceChanged'], - bysetpos = ['onRecurrenceChanged'], - bymonth = ['onRecurrenceChanged'], - bymonthday = ['onRecurrenceChanged'], - byyearday = ['onRecurrenceChanged'], - byweekno = ['onRecurrenceChanged'], - byhour = ['onRecurrenceChanged'], - byminute = ['onRecurrenceChanged'], - bysecond = ['onRecurrenceChanged'], - wkst = ['onRecurrenceChanged'], - byweekday = ['onRecurrenceChanged'], - freq = ['onRecurrenceChanged']) - # dateutil automatically sets these from dtstart, we don't want these # unless their length is greater than 1. interpretedNames = "byhour", "byminute", "bysecond" @@ -401,14 +390,7 @@ self.until = previous self.untilIsDate = False - def onRecurrenceChanged(self, name): - """If the rule changes, update any associated events.""" - if name in self.allNames: - for ruletype in ('rruleFor', 'exruleFor'): - if self.hasLocalAttributeValue(ruletype): - getattr(self, ruletype).onRuleSetChanged('rrules') - class RecurrenceRuleSet(items.ContentItem): """ A collection of recurrence and exclusion rules, dates, and exclusion dates. @@ -439,6 +421,16 @@ sharing = schema.Cloud(exdates, rdates, byCloud = [exrules, rrules]) ) + @schema.observer(rrules, exrules, rdates, exdates) + def onRuleSetChanged(self, name): + """If the RuleSet changes, update the associated event.""" + if not getattr(self, '_ignoreValueChanges', False): + if self.hasLocalAttributeValue('events'): + for event in self.events: + event.getFirstInRule().cleanRule() + # assume we have only one conceptual event per rrule + break + def addRule(self, rule, rrulesorexrules='rrules'): """Add an rrule or exrule, defaults to rrule. @@ -670,18 +662,4 @@ rule.moveUntilBefore(dtstart, end) self.removeDates(datetime.__ge__, end) - RULENAMES = ('rrules', 'exrules', 'rdates', 'exdates') - schema.afterChange(rrules = ['onRuleSetChanged'], - exrules = ['onRuleSetChanged'], - rdates = ['onRuleSetChanged'], - exdates = ['onRuleSetChanged']) - - def onRuleSetChanged(self, name): - """If the RuleSet changes, update the associated event.""" - if not getattr(self, '_ignoreValueChanges', False): - if self.hasLocalAttributeValue('events'): - for event in self.events: - event.getFirstInRule().cleanRule() - # assume we have only one conceptual event per rrule - break
Modified: trunk/chandler/parcels/osaf/pim/calendar/TimeZone.py (11436 => 11437)
--- trunk/chandler/parcels/osaf/pim/calendar/TimeZone.py 2006-08-17 18:54:38 UTC (rev 11436) +++ trunk/chandler/parcels/osaf/pim/calendar/TimeZone.py 2006-08-17 19:18:11 UTC (rev 11437) @@ -29,9 +29,23 @@ default = schema.One( schema.TimeZone, - afterChange = ['onDefaultChanged'] ) + @schema.observer(default) + def onDefaultChanged(self, name): + # Repository hook for attribute changes. + default = self.default + canonicalDefault = self.canonicalTimeZone(default) + # Make sure that PyICU's default timezone is synched with + # ours + if (canonicalDefault is not None and + canonicalDefault is not PyICU.ICUtzinfo.floating): + PyICU.ICUtzinfo.default = canonicalDefault + # This next if is required to avoid an infinite recursion! + if canonicalDefault is not default: + self.default = canonicalDefault + + # List of well-known time zones (for populating drop-downs). # [i18n] Since ICU doesn't suitably localize strings like 'US/Pacific', # we'll have to provide our own translations. @@ -120,18 +134,6 @@ if tz is not None and view is not None: PyICU.TimeZone.setDefault(tz.timezone) - def onDefaultChanged(self, name): - # Repository hook for attribute changes. - default = self.default - canonicalDefault = self.canonicalTimeZone(default) - # Make sure that PyICU's default timezone is synched with - # ours - if (canonicalDefault is not None and - canonicalDefault is not PyICU.ICUtzinfo.floating): - PyICU.ICUtzinfo.default = canonicalDefault - # This next if is required to avoid an infinite recursion! - if canonicalDefault is not default: - self.default = canonicalDefault def installParcel(parcel, oldVersion = None): # Get our parcel's namespace @@ -228,9 +230,9 @@ """ Return an empty string or the short timezone string for dt if dt.tzinfo doesn't match tzinfo (tzinfo defaults to PyICU.ICUtzinfo.default) - + """ - if tzinfo is None: tzinfo = PyICU.ICUtzinfo.default + if tzinfo is None: tzinfo = PyICU.ICUtzinfo.default if dt.tzinfo is None or dt.tzinfo is PyICU.ICUtzinfo.floating: return u''
Modified: trunk/chandler/projects/Chandler-PhotoPlugin/photos/Photos.py (11436 => 11437)
--- trunk/chandler/projects/Chandler-PhotoPlugin/photos/Photos.py 2006-08-17 18:54:38 UTC (rev 11436) +++ trunk/chandler/projects/Chandler-PhotoPlugin/photos/Photos.py 2006-08-17 19:18:11 UTC (rev 11437) @@ -35,8 +35,12 @@ dateTaken = schema.One(schema.DateTime) file = schema.One(schema.Text) exif = schema.Mapping(schema.Text, initialValue={}) - photoBody = schema.One(schema.Lob, afterChange=['onPhotoBodyChanged']) + photoBody = schema.One(schema.Lob) + @schema.observer(photoBody) + def onPhotoBodyChanged(self, attribute): + self.processEXIF() + about = schema.One(redirectTo = 'displayName') date = schema.One(redirectTo = 'dateTaken') who = schema.One(redirectTo = 'creator') @@ -104,10 +108,7 @@ logger.debug("Couldn't process EXIF of Photo %s (%s)" % \ (self.itsPath, e)) - def onPhotoBodyChanged(self, attribute): - self.processEXIF() - class Photo(PhotoMixin, pim.Note): pass @@ -146,7 +147,7 @@ photo.displayName = filename photo.creator = schema.ns("osaf.pim", self.itsView).currentContact.item photo.importFromFile(path) - + theApp.CallItemMethodAsync("MainView", 'setStatusMessage',"") return photo
_______________________________________________ Commits mailing list [email protected] http://lists.osafoundation.org/mailman/listinfo/commits
