def fillme(field=field, possiblefillers=possiblefillers): # For preference, use a filler that will certainly return clean information (i.e. sort by the number of dirty inputs and prefer the first - i.e. the one with fewer dirty inputs) for fillerfunction, dirtyinputs, anyinputsdirty in sorted([(f, dirties(), len(dirties()) > 0) for f, dirties in possiblefillers], using(lambda x: x[2])): if field not in fact or isblankfield(fact[field]) or anyinputsdirty: # Don't know what the last value was or it may have changed: recompute. # # We also recompute if the incoming field is blank: this can happen if we have # deleted the contents of a field, and then come back to update the fact by tabbing # away from a filled field. Now we have enough information to fill it, and musn't # just reuse the old version, which will be blank. # # Note that if the field was *generated* as blank one then it will have a marker # in it, and so we won't pointlessly recompute its blankness. log.info("Attempting to fill %s field -- dirty inputs are %s", field, dirtyinputs) result = fillerfunction() if result is None: log.info("Filling %s failed -- falling back on another method", field) continue dirty[field] = cond(field in fact, lambda: result != unmarkgeneratedfield(fact[field]), lambda: anyinputsdirty) return result else: # Last value must not have changed: retain it assert (field in fact and not anyinputsdirty) log.info("Retaining old field value for %s (blank: %s)", field, isblankfield(fact[field])) dirty[field] = False return unmarkgeneratedfield(fact[field]) # What if all of the possible updaters failed? Ideally we would not be in the graph at all, but it's too late for that. # All we can do is return None, and deal with this possibility later on. log.info("All possible updaters failed for the field %s", field) dirty[field] = False return None
def filledgraphforupdaters(all_updaters, fact, delta): graph = {} dirty = {} finddirties = lambda usings: [using for using in usings if dirty[using]] # The initial shell contains just the stuff that is non-generated or being set in this round. # Everything else will be generated based off of these values initiallyfilledfields = set([field for field in fact if not isgeneratedfield(field, fact[field]) and not isblankfield(fact[field])]).union(set(delta.keys())) for field in initiallyfilledfields: dirty[field] = field in delta graph[field] = (False, Thunk(lambda field=field: cond(field in delta, lambda: delta[field], lambda: fact[field]))) log.info("Initially filled graph fields: %r", dirty) # Remove useless updaters, and updaters that might confound a delta by updating a field from an # old field. For example, if we change the reading we want to regenerate the color field -- but this is no # good if we just use the updater that gets the reading from the expression! all_updaters = [updater for updater in all_updaters if updater[0] not in initiallyfilledfields] delta_updaters = maydependon(all_updaters, delta.keys()) # A complication is the presence of non-generated blank fields. We want to try and update these even if the # delta is empty (for example). To this end we need to make sure that the cut doesn't exclude updaters that # might potentially be depended on by an updater for a blank field. # # We also exclude updaters for any field that is going to get filled out by any delta_updater, because those # updaters should take priority. all_updaters = [updater for updater in all_updaters if all([updater[0] not in delta_field for delta_field, _, _ in delta_updaters])] blank_updaters = dependedonby(all_updaters, [field for field in fact if isblankfield(fact[field])]) # Yes, this really works! This is because Python has reference comparison semantics on functions. # However, this is the reason we need to use tuples of fields we depend on, rather than lists updaters = set(delta_updaters + blank_updaters) def shell(alreadyfilled): # Gather all the fields we are newly able to fill given the most recent changes # to the list of already filled things cannowfill = FactoryDict(lambda _: []) for updatewhat, updatefunction, updateusings in updaters: if updatewhat not in alreadyfilled and all([updateusing in alreadyfilled for updateusing in updateusings]): # We have to delay the computation of whether something is dirty or not until we are inside # the actual thunk for this particular field, hence all the thunking and lambdas here inputs = Thunk(lambda updateusings=updateusings: [graph[updateusing][1]() for updateusing in updateusings]) cannowfill[updatewhat].append((lambda inputs=inputs, updatefunction=updatefunction: updatefunction(*(inputs())), lambda inputs=inputs, updateusings=updateusings: seq(inputs(), lambda: finddirties(updateusings)))) # Check for quiescence if len(cannowfill) == 0: # NB: could do something with the unfilled set (self.updateablefields.difference(alreadyfilled)) here return # Set up each fillable graph field with a thunk computing the value for field, possiblefillers in cannowfill.items(): def fillme(field=field, possiblefillers=possiblefillers): # For preference, use a filler that will certainly return clean information (i.e. sort by the number of dirty inputs and prefer the first - i.e. the one with fewer dirty inputs) for fillerfunction, dirtyinputs, anyinputsdirty in sorted([(f, dirties(), len(dirties()) > 0) for f, dirties in possiblefillers], using(lambda x: x[2])): if field not in fact or isblankfield(fact[field]) or anyinputsdirty: # Don't know what the last value was or it may have changed: recompute. # # We also recompute if the incoming field is blank: this can happen if we have # deleted the contents of a field, and then come back to update the fact by tabbing # away from a filled field. Now we have enough information to fill it, and musn't # just reuse the old version, which will be blank. # # Note that if the field was *generated* as blank one then it will have a marker # in it, and so we won't pointlessly recompute its blankness. log.info("Attempting to fill %s field -- dirty inputs are %s", field, dirtyinputs) result = fillerfunction() if result is None: log.info("Filling %s failed -- falling back on another method", field) continue dirty[field] = cond(field in fact, lambda: result != unmarkgeneratedfield(fact[field]), lambda: anyinputsdirty) return result else: # Last value must not have changed: retain it assert (field in fact and not anyinputsdirty) log.info("Retaining old field value for %s (blank: %s)", field, isblankfield(fact[field])) dirty[field] = False return unmarkgeneratedfield(fact[field]) # What if all of the possible updaters failed? Ideally we would not be in the graph at all, but it's too late for that. # All we can do is return None, and deal with this possibility later on. log.info("All possible updaters failed for the field %s", field) dirty[field] = False return None graph[field] = (True, Thunk(fillme)) shell(alreadyfilled.union(set(cannowfill.keys()))) shell(initiallyfilledfields) return graph