_discover.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import collections
  2. import inspect
  3. from automat import MethodicalMachine
  4. from twisted.python.modules import PythonModule, getModule
  5. def isOriginalLocation(attr):
  6. """
  7. Attempt to discover if this appearance of a PythonAttribute
  8. representing a class refers to the module where that class was
  9. defined.
  10. """
  11. sourceModule = inspect.getmodule(attr.load())
  12. if sourceModule is None:
  13. return False
  14. currentModule = attr
  15. while not isinstance(currentModule, PythonModule):
  16. currentModule = currentModule.onObject
  17. return currentModule.name == sourceModule.__name__
  18. def findMachinesViaWrapper(within):
  19. """
  20. Recursively yield L{MethodicalMachine}s and their FQPNs within a
  21. L{PythonModule} or a L{twisted.python.modules.PythonAttribute}
  22. wrapper object.
  23. Note that L{PythonModule}s may refer to packages, as well.
  24. The discovery heuristic considers L{MethodicalMachine} instances
  25. that are module-level attributes or class-level attributes
  26. accessible from module scope. Machines inside nested classes will
  27. be discovered, but those returned from functions or methods will not be.
  28. @type within: L{PythonModule} or L{twisted.python.modules.PythonAttribute}
  29. @param within: Where to start the search.
  30. @return: a generator which yields FQPN, L{MethodicalMachine} pairs.
  31. """
  32. queue = collections.deque([within])
  33. visited = set()
  34. while queue:
  35. attr = queue.pop()
  36. value = attr.load()
  37. if isinstance(value, MethodicalMachine) and value not in visited:
  38. visited.add(value)
  39. yield attr.name, value
  40. elif (inspect.isclass(value) and isOriginalLocation(attr) and
  41. value not in visited):
  42. visited.add(value)
  43. queue.extendleft(attr.iterAttributes())
  44. elif isinstance(attr, PythonModule) and value not in visited:
  45. visited.add(value)
  46. queue.extendleft(attr.iterAttributes())
  47. queue.extendleft(attr.iterModules())
  48. class InvalidFQPN(Exception):
  49. """
  50. The given FQPN was not a dot-separated list of Python objects.
  51. """
  52. class NoModule(InvalidFQPN):
  53. """
  54. A prefix of the FQPN was not an importable module or package.
  55. """
  56. class NoObject(InvalidFQPN):
  57. """
  58. A suffix of the FQPN was not an accessible object
  59. """
  60. def wrapFQPN(fqpn):
  61. """
  62. Given an FQPN, retrieve the object via the global Python module
  63. namespace and wrap it with a L{PythonModule} or a
  64. L{twisted.python.modules.PythonAttribute}.
  65. """
  66. # largely cribbed from t.p.reflect.namedAny
  67. if not fqpn:
  68. raise InvalidFQPN("FQPN was empty")
  69. components = collections.deque(fqpn.split('.'))
  70. if '' in components:
  71. raise InvalidFQPN(
  72. "name must be a string giving a '.'-separated list of Python "
  73. "identifiers, not %r" % (fqpn,))
  74. component = components.popleft()
  75. try:
  76. module = getModule(component)
  77. except KeyError:
  78. raise NoModule(component)
  79. # find the bottom-most module
  80. while components:
  81. component = components.popleft()
  82. try:
  83. module = module[component]
  84. except KeyError:
  85. components.appendleft(component)
  86. break
  87. else:
  88. module.load()
  89. else:
  90. return module
  91. # find the bottom-most attribute
  92. attribute = module
  93. for component in components:
  94. try:
  95. attribute = next(child for child in attribute.iterAttributes()
  96. if child.name.rsplit('.', 1)[-1] == component)
  97. except StopIteration:
  98. raise NoObject('{}.{}'.format(attribute.name, component))
  99. return attribute
  100. def findMachines(fqpn):
  101. """
  102. Recursively yield L{MethodicalMachine}s and their FQPNs in and
  103. under the a Python object specified by an FQPN.
  104. The discovery heuristic considers L{MethodicalMachine} instances
  105. that are module-level attributes or class-level attributes
  106. accessible from module scope. Machines inside nested classes will
  107. be discovered, but those returned from functions or methods will not be.
  108. @type within: an FQPN
  109. @param within: Where to start the search.
  110. @return: a generator which yields FQPN, L{MethodicalMachine} pairs.
  111. """
  112. return findMachinesViaWrapper(wrapFQPN(fqpn))