Source code for coalib.core.DependencyTracker

from itertools import chain

from coalib.core.Graphs import traverse_graph


[docs]class DependencyTracker: """ A ``DependencyTracker`` allows to register and manage dependencies between objects. This class uses a directed graph to track relations. Add a dependency relation between two objects: >>> object1 = object() >>> object2 = object() >>> tracker = DependencyTracker() >>> tracker.add(object2, object1) This would define that ``object1`` is dependent on ``object2``. If you define that ``object2`` has its dependency duty fulfilled, you can resolve it: >>> resolved = tracker.resolve(object2) >>> resolved {<object object at ...>} >>> resolved_object = resolved.pop() >>> resolved_object is object1 True This returns all objects that are now freed, meaning they have no dependencies any more. >>> object3 = object() >>> tracker.add(object2, object1) >>> tracker.add(object3, object1) >>> tracker.resolve(object2) set() >>> tracker.resolve(object3) {<object object at ...>} The ones who instantiate a ``DependencyTracker`` are responsible for resolving dependencies in the right order. Dependencies which are itself dependent will be forcefully resolved and removed from their according dependencies too. """ def __init__(self): self._dependency_dict = {}
[docs] def get_dependants(self, dependency): """ Returns all immediate dependants for the given dependency. >>> tracker = DependencyTracker() >>> tracker.add(0, 1) >>> tracker.add(0, 2) >>> tracker.add(1, 3) >>> tracker.get_dependants(0) {1, 2} >>> tracker.get_dependants(1) {3} >>> tracker.get_dependants(2) set() :param dependency: The dependency to retrieve all dependants from. :return: A set of dependants. """ try: return set(self._dependency_dict[dependency]) except KeyError: return set()
[docs] def get_dependencies(self, dependant): """ Returns all immediate dependencies of a given dependant. >>> tracker = DependencyTracker() >>> tracker.add(0, 1) >>> tracker.add(0, 2) >>> tracker.add(1, 2) >>> tracker.get_dependencies(0) set() >>> tracker.get_dependencies(1) {0} >>> tracker.get_dependencies(2) {0, 1} :param dependant: The dependant to retrieve all dependencies from. :return: A set of dependencies. """ return set( dependency for dependency, dependants in self._dependency_dict.items() if dependant in dependants)
[docs] def get_all_dependants(self, dependency): """ Returns a set of all dependants of the given dependency, even indirectly related ones. >>> tracker = DependencyTracker() >>> tracker.add(0, 1) >>> tracker.add(1, 2) >>> tracker.get_all_dependants(0) {1, 2} :param dependency: The dependency to get all dependants for. :return: A set of dependants. """ dependants = set() def append_to_dependants(prev, nxt): dependants.add(nxt) traverse_graph( [dependency], lambda node: self._dependency_dict.get(node, frozenset()), append_to_dependants) return dependants
[docs] def get_all_dependencies(self, dependant): """ Returns a set of all dependencies of the given dependants, even indirectly related ones. >>> tracker = DependencyTracker() >>> tracker.add(0, 1) >>> tracker.add(1, 2) >>> tracker.get_all_dependencies(2) {0, 1} :param dependant: The dependant to get all dependencies for. :return: A set of dependencies. """ dependencies = set() def append_to_dependencies(prev, nxt): dependencies.add(nxt) traverse_graph( [dependant], lambda node: {dependency for dependency, dependants in self._dependency_dict.items() if node in dependants}, append_to_dependencies) return dependencies
@property def dependants(self): """ Returns a set of all registered dependants. >>> tracker = DependencyTracker() >>> tracker.add(0, 1) >>> tracker.add(0, 2) >>> tracker.add(1, 3) >>> tracker.dependants {1, 2, 3} """ return set(chain.from_iterable(self._dependency_dict.values())) @property def dependencies(self): """ Returns a set of all registered dependencies. >>> tracker = DependencyTracker() >>> tracker.add(0, 1) >>> tracker.add(0, 2) >>> tracker.add(1, 3) >>> tracker.dependencies {0, 1} """ return set(self._dependency_dict.keys()) def __iter__(self): """ Returns an iterator that iterates over all dependency relations. >>> tracker = DependencyTracker() >>> tracker.add(0, 1) >>> tracker.add(0, 2) >>> tracker.add(1, 2) >>> for dependency, dependant in sorted(tracker): ... print(dependency, '->', dependant) 0 -> 1 0 -> 2 1 -> 2 """ return ((dependency, dependant) for dependency, dependants in self._dependency_dict.items() for dependant in dependants)
[docs] def add(self, dependency, dependant): """ Add a dependency relation. This function does not check for circular dependencies. >>> tracker = DependencyTracker() >>> tracker.add(0, 1) >>> tracker.add(0, 2) >>> tracker.resolve(0) {1, 2} :param dependency: The object that is the dependency. :param dependant: The object that is the dependant. """ if dependency not in self._dependency_dict: self._dependency_dict[dependency] = set() self._dependency_dict[dependency].add(dependant)
[docs] def resolve(self, dependency): """ Resolves all dependency-relations from the given dependency, and frees and returns dependants with no more dependencies. If the given dependency is itself a dependant, all those relations are also removed. >>> tracker = DependencyTracker() >>> tracker.add(0, 1) >>> tracker.add(0, 2) >>> tracker.add(2, 3) >>> tracker.resolve(0) {1, 2} >>> tracker.resolve(2) {3} >>> tracker.resolve(2) set() :param dependency: The dependency. :return: Returns a set of dependants whose dependencies were all resolved. """ # Check if dependency has itself dependencies which aren't resolved, # these need to be removed too. This operation does not free any # dependencies. dependencies_to_remove = [] for tracked_dependency, dependants in self._dependency_dict.items(): if dependency in dependants: dependants.remove(dependency) # If dependants set is now empty, schedule dependency for # removal from dependency_dict. if not dependants: dependencies_to_remove.append(tracked_dependency) for tracked_dependency in dependencies_to_remove: del self._dependency_dict[tracked_dependency] # Now free dependants which do depend on the given dependency. possible_freed_dependants = self._dependency_dict.pop( dependency, set()) non_free_dependants = set() for possible_freed_dependant in possible_freed_dependants: # Check if all dependencies of dependants from above are satisfied. # If so, there are no more dependencies for dependant. Thus it's # resolved. for dependants in self._dependency_dict.values(): if possible_freed_dependant in dependants: non_free_dependants.add(possible_freed_dependant) break # Remaining dependents are officially resolved. return possible_freed_dependants - non_free_dependants
[docs] def check_circular_dependencies(self): """ Checks whether there are circular dependency conflicts. >>> tracker = DependencyTracker() >>> tracker.add(0, 1) >>> tracker.add(1, 0) >>> tracker.check_circular_dependencies() Traceback (most recent call last): ... coalib.core.CircularDependencyError.CircularDependencyError: ... :raises CircularDependencyError: Raised on circular dependency conflicts. """ traverse_graph( self._dependency_dict.keys(), lambda node: self._dependency_dict.get(node, frozenset()))
@property def are_dependencies_resolved(self): """ Checks whether all dependencies in this ``DependencyTracker`` instance are resolved. >>> tracker = DependencyTracker() >>> tracker.are_dependencies_resolved True >>> tracker.add(0, 1) >>> tracker.are_dependencies_resolved False >>> tracker.resolve(0) {1} >>> tracker.are_dependencies_resolved True :return: ``True`` when all dependencies resolved, ``False`` if not. """ return not self._dependency_dict