depstree.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. # Copyright 2009 The Closure Library Authors. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS-IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Class to represent a full Closure Library dependency tree.
  15. Offers a queryable tree of dependencies of a given set of sources. The tree
  16. will also do logical validation to prevent duplicate provides and circular
  17. dependencies.
  18. """
  19. __author__ = 'nnaze@google.com (Nathan Naze)'
  20. class DepsTree(object):
  21. """Represents the set of dependencies between source files."""
  22. def __init__(self, sources):
  23. """Initializes the tree with a set of sources.
  24. Args:
  25. sources: A set of JavaScript sources.
  26. Raises:
  27. MultipleProvideError: A namespace is provided by muplitple sources.
  28. NamespaceNotFoundError: A namespace is required but never provided.
  29. """
  30. self._sources = sources
  31. self._provides_map = dict()
  32. # Ensure nothing was provided twice.
  33. for source in sources:
  34. for provide in source.provides:
  35. if provide in self._provides_map:
  36. raise MultipleProvideError(
  37. provide, [self._provides_map[provide], source])
  38. self._provides_map[provide] = source
  39. # Check that all required namespaces are provided.
  40. for source in sources:
  41. for require in source.requires:
  42. if require not in self._provides_map:
  43. raise NamespaceNotFoundError(require, source)
  44. def GetDependencies(self, required_namespaces):
  45. """Get source dependencies, in order, for the given namespaces.
  46. Args:
  47. required_namespaces: A string (for one) or list (for one or more) of
  48. namespaces.
  49. Returns:
  50. A list of source objects that provide those namespaces and all
  51. requirements, in dependency order.
  52. Raises:
  53. NamespaceNotFoundError: A namespace is requested but doesn't exist.
  54. CircularDependencyError: A cycle is detected in the dependency tree.
  55. """
  56. if isinstance(required_namespaces, str):
  57. required_namespaces = [required_namespaces]
  58. deps_sources = []
  59. for namespace in required_namespaces:
  60. for source in DepsTree._ResolveDependencies(
  61. namespace, [], self._provides_map, []):
  62. if source not in deps_sources:
  63. deps_sources.append(source)
  64. return deps_sources
  65. @staticmethod
  66. def _ResolveDependencies(required_namespace, deps_list, provides_map,
  67. traversal_path):
  68. """Resolve dependencies for Closure source files.
  69. Follows the dependency tree down and builds a list of sources in dependency
  70. order. This function will recursively call itself to fill all dependencies
  71. below the requested namespaces, and then append its sources at the end of
  72. the list.
  73. Args:
  74. required_namespace: String of required namespace.
  75. deps_list: List of sources in dependency order. This function will append
  76. the required source once all of its dependencies are satisfied.
  77. provides_map: Map from namespace to source that provides it.
  78. traversal_path: List of namespaces of our path from the root down the
  79. dependency/recursion tree. Used to identify cyclical dependencies.
  80. This is a list used as a stack -- when the function is entered, the
  81. current namespace is pushed and popped right before returning.
  82. Each recursive call will check that the current namespace does not
  83. appear in the list, throwing a CircularDependencyError if it does.
  84. Returns:
  85. The given deps_list object filled with sources in dependency order.
  86. Raises:
  87. NamespaceNotFoundError: A namespace is requested but doesn't exist.
  88. CircularDependencyError: A cycle is detected in the dependency tree.
  89. """
  90. source = provides_map.get(required_namespace)
  91. if not source:
  92. raise NamespaceNotFoundError(required_namespace)
  93. if required_namespace in traversal_path:
  94. traversal_path.append(required_namespace) # do this *after* the test
  95. # This must be a cycle.
  96. raise CircularDependencyError(traversal_path)
  97. # If we don't have the source yet, we'll have to visit this namespace and
  98. # add the required dependencies to deps_list.
  99. if source not in deps_list:
  100. traversal_path.append(required_namespace)
  101. for require in source.requires:
  102. # Append all other dependencies before we append our own.
  103. DepsTree._ResolveDependencies(require, deps_list, provides_map,
  104. traversal_path)
  105. deps_list.append(source)
  106. traversal_path.pop()
  107. return deps_list
  108. class BaseDepsTreeError(Exception):
  109. """Base DepsTree error."""
  110. def __init__(self):
  111. Exception.__init__(self)
  112. class CircularDependencyError(BaseDepsTreeError):
  113. """Raised when a dependency cycle is encountered."""
  114. def __init__(self, dependency_list):
  115. BaseDepsTreeError.__init__(self)
  116. self._dependency_list = dependency_list
  117. def __str__(self):
  118. return ('Encountered circular dependency:\n%s\n' %
  119. '\n'.join(self._dependency_list))
  120. class MultipleProvideError(BaseDepsTreeError):
  121. """Raised when a namespace is provided more than once."""
  122. def __init__(self, namespace, sources):
  123. BaseDepsTreeError.__init__(self)
  124. self._namespace = namespace
  125. self._sources = sources
  126. def __str__(self):
  127. source_strs = map(str, self._sources)
  128. return ('Namespace "%s" provided more than once in sources:\n%s\n' %
  129. (self._namespace, '\n'.join(source_strs)))
  130. class NamespaceNotFoundError(BaseDepsTreeError):
  131. """Raised when a namespace is requested but not provided."""
  132. def __init__(self, namespace, source=None):
  133. BaseDepsTreeError.__init__(self)
  134. self._namespace = namespace
  135. self._source = source
  136. def __str__(self):
  137. msg = 'Namespace "%s" never provided.' % self._namespace
  138. if self._source:
  139. msg += ' Required in %s' % self._source
  140. return msg