def _prune_to_scope()

in objectModel/Python/cdm/objectmodel/cdm_attribute_context.py [0:0]


    def _prune_to_scope(self, scope_set: Set['CdmAttributeContext']) -> bool:
        # run over the whole tree and make a set of the nodes that should be saved for sure. This is anything NOT under a generated set 
        # (so base entity chains, entity attributes entity definitions)

        # for testing, don't delete this
        # def count_nodes(sub_item) -> int:
        #    if not isinstance(sub_item, CdmAttributeContext):
        #        return 1
        #    ac = sub_item  # type: CdmAttributeContext
        #    if not ac.contents:
        #        return 1
        #    # look at all children
        #    total = 0
        #    for sub_sub in ac.contents:
        #        total += count_nodes(sub_sub)
        #    return 1 + total
        # print('Pre Prune', count_nodes(self))


        # so ... the change from the old behavior is to depend on the lineage pointers to save the attribute defs
        # in the 'structure' part of the tree that might matter. keep all of the other structure info and keep some 
        # special nodes (like the ones that have removed attributes) that won't get found from lineage trace but that are
        # needed to understand what took place in resolution
        nodes_to_save = set()

        # helper that save the passed node and anything up the parent chain 
        def save_parent_nodes(curr_node) -> bool:
            if curr_node in nodes_to_save:
                return True
            nodes_to_save.add(curr_node)
            # get the parent 
            if curr_node.parent and curr_node.parent.explicit_reference:
                return save_parent_nodes(curr_node.parent.explicit_reference)
            return True

        # helper that saves the current node (and parents) plus anything in the lineage (with their parents)
        def save_lineage_nodes(curr_node) -> bool:
            if not save_parent_nodes(curr_node):
                return False

            if curr_node.lineage:
                for lin in curr_node.lineage:
                    if lin.explicit_reference:
                        if not save_lineage_nodes(lin.explicit_reference):
                            return False
            return True

        def save_structure_nodes(sub_item, in_generated, in_projection, in_remove) -> bool:
            if not isinstance(sub_item, CdmAttributeContext):
                return True

            ac = sub_item  # type: CdmAttributeContext
            if ac.type == CdmAttributeContextType.GENERATED_SET:
                in_generated = True # special mode where we hate everything except the removed att notes

            if in_generated and ac.type == CdmAttributeContextType.OPERATION_EXCLUDE_ATTRIBUTES:
                in_remove = True # triggers us to know what to do in the next code block.
            removed_attribute = False
            if ac.type == CdmAttributeContextType.ATTRIBUTE_DEFINITION:
                # empty attribute nodes are descriptions of source attributes that may or may not be needed. lineage will sort it out.
                # the exception is for attribute descriptions under a remove attributes operation. they are gone from the resolved att set, so
                # no history would remain 
                if in_remove:
                    removed_attribute = True
                else:
                    if ac.contents is None or len(ac.contents) == 0:
                        return True

            # this attribute was removed by a projection operation, but we want to keep the node to indicate what the operation did
            if ac.type == CdmAttributeContextType.ATTRIBUTE_EXCLUDED:
                removed_attribute = True

            if not in_generated or removed_attribute:
                # mark this as something worth saving, sometimes 
                # these get discovered at the leaf of a tree that we want to mostly ignore, so can cause a
                # discontinuity in the 'save' chains, so fix that
                save_lineage_nodes(ac)

            if ac.type == CdmAttributeContextType.PROJECTION:
                in_projection = True # track this so we can do the next thing ...

            if ac.type == CdmAttributeContextType.ENTITY and in_projection:
                # this is far enough, the entity that is somewhere under a projection chain
                # things under this might get saved through lineage, but down to this point will get in for sure
                return True

            if ac.contents is None or len(ac.contents) == 0:
                return True

            # look at all children
            for sub_sub in ac.contents:
                if not save_structure_nodes(sub_sub, in_generated, in_projection, in_remove):
                    return False
            return True

        if not save_structure_nodes(self, False, False, False):
            return False

        # next, look at the attCtx for every resolved attribute. follow the lineage chain and mark all of those nodes as ones to save
        # also mark any parents of those as savers


        # so, do that ^^^ for every primary context found earlier
        for prim_ctx in scope_set:
            if not save_lineage_nodes(prim_ctx):
                return False

        # now the cleanup, we have a set of the nodes that should be saved
        # run over the tree and re-build the contents collection with only the things to save
        def clean_sub_group(sub_item) -> bool:
            if sub_item.object_type == CdmObjectType.ATTRIBUTE_REF:
                return True # not empty

            ac = sub_item  # type: CdmAttributeContext

            if ac not in nodes_to_save:
                return False # don't even look at content, this all goes away

            if ac.contents:
                # need to clean up the content array without triggering the code that fixes in document or paths
                new_content = []  # type: List[CdmObject]
                for sub in ac.contents:
                    # true means keep this as a child
                    if clean_sub_group(sub):
                        new_content.append(sub)
                # clear the old content and replace
                ac.contents.clear()
                ac.contents.extend(new_content)

            return True

        clean_sub_group(self)

        # print('Post Prune', count_nodes(self))

        return True