I don’t understand why the _ResolveDependencies method in DepsTree class can detect circular dependencies. Seems it can detect the situation if a requires b and e and b requires e, but it’s not a circular dependencies.
class DepsTree(object):
"""Represents the set of dependencies between source files."""
def __init__(self, sources):
"""Initializes the tree with a set of sources.
Args:
sources: A set of JavaScript sources.
Raises:
MultipleProvideError: A namespace is provided by muplitple sources.
NamespaceNotFoundError: A namespace is required but never provided.
"""
self._sources = sources
self._provides_map = dict()
# Ensure nothing was provided twice.
for source in sources:
for provide in source.provides:
if provide in self._provides_map:
raise MultipleProvideError(
provide, [self._provides_map[provide], source])
self._provides_map[provide] = source
# Check that all required namespaces are provided.
for source in sources:
for require in source.requires:
if require not in self._provides_map:
raise NamespaceNotFoundError(require, source)
def GetDependencies(self, required_namespaces):
"""Get source dependencies, in order, for the given namespaces.
Args:
required_namespaces: A string (for one) or list (for one or more) of
namespaces.
Returns:
A list of source objects that provide those namespaces and all
requirements, in dependency order.
Raises:
NamespaceNotFoundError: A namespace is requested but doesn't exist.
CircularDependencyError: A cycle is detected in the dependency tree.
"""
if type(required_namespaces) is str:
required_namespaces = [required_namespaces]
deps_sources = []
for namespace in required_namespaces:
for source in DepsTree._ResolveDependencies(
namespace, [], self._provides_map, []):
if source not in deps_sources:
deps_sources.append(source)
return deps_sources
@staticmethod
def _ResolveDependencies(required_namespace, deps_list, provides_map,
traversal_path):
"""Resolve dependencies for Closure source files.
Follows the dependency tree down and builds a list of sources in dependency
order. This function will recursively call itself to fill all dependencies
below the requested namespaces, and then append its sources at the end of
the list.
Args:
required_namespace: String of required namespace.
deps_list: List of sources in dependency order. This function will append
the required source once all of its dependencies are satisfied.
provides_map: Map from namespace to source that provides it.
traversal_path: List of namespaces of our path from the root down the
dependency/recursion tree. Used to identify cyclical dependencies.
This is a list used as a stack -- when the function is entered, the
current namespace is pushed and popped right before returning.
Each recursive call will check that the current namespace does not
appear in the list, throwing a CircularDependencyError if it does.
Returns:
The given deps_list object filled with sources in dependency order.
Raises:
NamespaceNotFoundError: A namespace is requested but doesn't exist.
CircularDependencyError: A cycle is detected in the dependency tree.
"""
source = provides_map.get(required_namespace)
if not source:
raise NamespaceNotFoundError(required_namespace)
if required_namespace in traversal_path:
traversal_path.append(required_namespace) # do this *after* the test
# This must be a cycle.
raise CircularDependencyError(traversal_path)
traversal_path.append(required_namespace)
for require in source.requires:
# Append all other dependencies before we append our own.
DepsTree._ResolveDependencies(require, deps_list, provides_map,
traversal_path)
deps_list.append(source)
traversal_path.pop()
return deps_list
Short version:
_ResolveDependenciesperforms a depth-first traversal of the dependency tree, recording the path. If it encounters a node that is already in the path, this means there’s a cycle._ResolveDependenciestraverses the dependency forest embodied bySource.requires. The active calls to_ResolveDependenciesat any point in time corresponds to a path through the dependency tree (hence the_pathintraversal_path); this is tracked by adding a namespace totraversal_pathbefore recursion and removing it after. In other words, a namespace is intraversal_pathif and only if an invocation of_ResolveDependenciesis processing the namespace. If_ResolveDependenciesis asked to check a namespace that already exists intraversal_path, then a different call to_ResolveDependenciesis processing the namespace, and there exists a path from a node to itself, hence a cycle.As an example, consider the simplest dependency cycle: “a” requires “c” requires “a”. Let’s also throw in an “a” requires “b” to show what happens when there isn’t a dependency in a branch. You’d get the following call sequence (in pseudo-python). Most of the time the value of a variable is substituted for the variable name (e.g.
aforns.name). Note: this doesn’t represent source code but rather the sequence of statements that are executed when the program runs._RD(ns={name:a, req: [b, c]}, path=[]): if a in path: # false path += [a] for each subns in [b,c]: _RD(ns={name: b, req: []}, path=[a]): if b in path: # false path += [b] for each subns in []: # done with this branch path -= [b] _RD(ns={name: c, req: [a]}, path=[a]): if c in path: # false path += [c] for each subns in [a]: _RD(ns={name: a req: [b,c]}, path=[a, c]): if a in path: # true throw CircularDependencyError