docs: def dirty(field,ttl=None):
    "decorator to cache the result of a function until a field changes"
    if ttl is not None: raise NotImplementedError('pg.dirty ttl feature')
    def decorator(f):
      @functools.wraps(f)
      def wrapper(self,*args,**kwargs):
        # warning: not reentrant
        d=self.dirty_cache[field] if field in self.dirty_cache else self.dirty_cache.setdefault(field,{})
        return d[f.__name__] if f.__name__ in d else d.setdefault(f.__name__,f(self,*args,**kwargs))
      return wrapper
    return decorator