def updatefact(self, fact, expression, **kwargs): # AutoBlanking Feature - If there is no expression, zeros relevant fields if expression != None and expression.strip() == u"": for field in self.graphbasedupdater.updateablefields: if field in fact and factproxy.isgeneratedfield(field, fact[field]): fact[field] = u"" # TODO: Nick added this to give up after auto-blanking. He claims it removes a minor # delay, but I'm not sure where the delay originates from, which worries me: return FieldUpdater.updatefact(self, fact, expression, **kwargs)
def updatefact(self, fact, value, alwaysreformat=False): assert self.field in fact assert value is None or not(factproxy.isgeneratedfield(self.field, value)) # HACK ALERT: can't think of a nicer way to do mwfieldinfact # NB: this is not quite right. The user might have changed whether there is a mw field independently of whether the # contents of the field we are considering has changed. However, this bug should be very hard to hit (TODO: fix it). delta = value is not None and { self.field : value, "mwfieldinfact" : "mw" in fact } or {} graph = self.graphbasedupdater.filledgraph(fact, delta) # For the same reason that fields may be missing from the graph but present in the fact, # even the field we are updating can be missing. For example, this happens when tabbing # away from a generated field. # # We should also avoid doing this if the field is considered generated, because in that # case reformatting leads to changing the field to be spuriously marked as original. if self.field in graph and not(factproxy.isgeneratedfield(self.field, value or fact[self.field])): # EAGERLY reformat to produce the new value, which we plop back into the graph again so # other computations can see it. Note that this is potentially unsafe because we might now actually # need to recompute other thunks in the graph that depended on this one. Thankfully, right now # we only have updaters that guarantee not to change the value of an expression which will change # the value of anything *they* force to FIND that new value, so it works out. fact[self.field] = self.reformatter.reformatfield(self.field, graph, alwaysreformat=alwaysreformat) graph[self.field] = (False, utils.Thunk(lambda: fact[self.field])) log.info("Keys in the graph after update: %r", graph.keys()) for field in fact: # Fields can be missing from the graph if they are generated fields in the input fact that # the graph failed to find a way to update. This really does happen! if shouldupdatefield(self.graphbasedupdater.config)(field) and field in graph: value = graph[field][1]() # The value can be None if we tried to update the field but just couldn't find any information. # This can happen e.g. with the "meaning" field if there is no definition in the dictionary. if value is not None: # Last-ditch attempt to convert to unicode to intercept any ASCII that slipped though the net fact[field] = (graph[field][0] and factproxy.markgeneratedfield or (lambda x: x))(unicode(value))
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