_pydoctor.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. # -*- test-case-name: twisted.python.test.test_pydoctor -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Support for a few things specific to documenting Twisted using pydoctor.
  6. FIXME: https://github.com/twisted/pydoctor/issues/106
  7. This documentation does not link to pydoctor API as there is no public API yet.
  8. """
  9. import urllib2
  10. from compiler import ast
  11. from pydoctor import model, zopeinterface
  12. from pydoctor.sphinx import SphinxInventory
  13. class HeadRequest(urllib2.Request, object):
  14. """
  15. A request for the HEAD HTTP method.
  16. """
  17. def get_method(self):
  18. """
  19. Use the HEAD HTTP method.
  20. """
  21. return 'HEAD'
  22. class TwistedSphinxInventory(SphinxInventory):
  23. """
  24. Custom SphinxInventory to work around broken external references to
  25. Sphinx.
  26. All exceptions should be reported upstream and a comment should be created
  27. with a link to the upstream report.
  28. """
  29. def getLink(self, name):
  30. """
  31. Resolve the full URL for a cross reference.
  32. @param name: Value of the cross reference.
  33. @type name: L{str}
  34. @return: A full URL for the I{name} reference or L{None} if no link was
  35. found.
  36. @rtype: L{str} or L{None}
  37. """
  38. result = super(TwistedSphinxInventory, self).getLink(name)
  39. if result is not None:
  40. # We already got a link. Look no further.
  41. return result
  42. if name.startswith('zope.interface.'):
  43. # This is a link from zope.interface. which is not advertised in
  44. # the Sphinx inventory.
  45. # See if the link is a known broken link which should be handled
  46. # as an exceptional case.
  47. # We get the base URL from IInterface which is assume that is
  48. # always and already well defined in the Sphinx index.
  49. baseURL, _ = self._links.get(
  50. 'zope.interface.interfaces.IInterface',
  51. (None, None))
  52. if baseURL is None:
  53. # Most probably the zope.interface inventory was
  54. # not loaded.
  55. return None
  56. if name == 'zope.interface.adapter.AdapterRegistry':
  57. # FIXME:
  58. # https://github.com/zopefoundation/zope.interface/issues/41
  59. relativeLink = 'adapter.html'
  60. else:
  61. # Not a known exception.
  62. relativeLink = None
  63. if relativeLink is None:
  64. return None
  65. return '%s/%s' % (baseURL, relativeLink)
  66. if name.startswith('win32api'):
  67. # This is a link to pywin32 which does not provide inter-API doc
  68. # link capabilities
  69. baseURL = 'http://docs.activestate.com/activepython/2.7/pywin32'
  70. # For now only links to methods are supported.
  71. relativeLink = '%s_meth.html' % (name.replace('.', '__'),)
  72. fullURL = '%s/%s' % (baseURL, relativeLink)
  73. # Check if URL exists.
  74. response = self._getURLAsHEAD(fullURL)
  75. if response:
  76. if response.code == 200:
  77. return fullURL
  78. else:
  79. # Bad URL resolution.
  80. print("BAD URL resolution, code: ", response.code)
  81. return None
  82. def _getURLAsHEAD(self, url):
  83. """
  84. Get are HEAD response for URL.
  85. Here to help with testing and allow injecting another URL getter.
  86. @param url: Full URL to the page which is retrieved.
  87. @type url: L{str}
  88. @return: The response for the HEAD method.
  89. @rtype: urllib2 response or L{None}
  90. """
  91. try:
  92. return urllib2.urlopen(HeadRequest(url))
  93. except Exception as e:
  94. print("Error opening {}: {}".format(url, e))
  95. return None
  96. def getDeprecated(self, decorators):
  97. """
  98. With a list of decorators, and the object it is running on, set the
  99. C{_deprecated_info} flag if any of the decorators are a Twisted deprecation
  100. decorator.
  101. """
  102. for a in decorators:
  103. if isinstance(a, ast.CallFunc):
  104. decorator = a.asList()
  105. # Getattr is used when the decorator is @foo.bar, not @bar
  106. if isinstance(decorator[0], ast.Getattr):
  107. getAttr = decorator[0].asList()
  108. name = getAttr[0].name
  109. fn = self.expandName(name) + "." + getAttr[1]
  110. else:
  111. fn = self.expandName(decorator[0].name)
  112. if fn == "twisted.python.deprecate.deprecated":
  113. try:
  114. self._deprecated_info = deprecatedToUsefulText(
  115. self.name, decorator)
  116. except AttributeError:
  117. # It's a reference or something that we can't figure out
  118. # from the AST.
  119. pass
  120. class TwistedModuleVisitor(zopeinterface.ZopeInterfaceModuleVisitor):
  121. def visitClass(self, node):
  122. """
  123. Called when a class is visited.
  124. """
  125. super(TwistedModuleVisitor, self).visitClass(node)
  126. cls = self.builder.current.contents[node.name]
  127. getDeprecated(cls, list(cls.raw_decorators))
  128. def visitFunction(self, node):
  129. """
  130. Called when a class is visited.
  131. """
  132. super(TwistedModuleVisitor, self).visitFunction(node)
  133. func = self.builder.current.contents[node.name]
  134. if func.decorators:
  135. getDeprecated(func, list(func.decorators))
  136. def versionToUsefulObject(version):
  137. """
  138. Change an AST C{Version()} to a real one.
  139. """
  140. from incremental import Version
  141. return Version(*[x.value for x in version.asList()[1:] if x])
  142. def deprecatedToUsefulText(name, deprecated):
  143. """
  144. Change a C{@deprecated} to a display string.
  145. """
  146. from twisted.python.deprecate import _getDeprecationWarningString
  147. version = versionToUsefulObject(deprecated[1])
  148. if deprecated[2]:
  149. if isinstance(deprecated[2], ast.Keyword):
  150. replacement = deprecated[2].asList()[1].value
  151. else:
  152. replacement = deprecated[2].value
  153. else:
  154. replacement = None
  155. return _getDeprecationWarningString(name, version, replacement=replacement) + "."
  156. class TwistedFunction(zopeinterface.ZopeInterfaceFunction):
  157. def docsources(self):
  158. if self.decorators:
  159. getDeprecated(self, list(self.decorators))
  160. for x in super(TwistedFunction, self).docsources():
  161. yield x
  162. class TwistedASTBuilder(zopeinterface.ZopeInterfaceASTBuilder):
  163. # Vistor is not a typo...
  164. ModuleVistor = TwistedModuleVisitor
  165. class TwistedSystem(zopeinterface.ZopeInterfaceSystem):
  166. """
  167. A PyDoctor "system" used to generate the docs.
  168. """
  169. defaultBuilder = TwistedASTBuilder
  170. Function = TwistedFunction
  171. def __init__(self, options=None):
  172. super(TwistedSystem, self).__init__(options=options)
  173. # Use custom SphinxInventory so that we can resolve valid L{} markup
  174. # for which the Sphinx inventory is not published or broken.
  175. self.intersphinx = TwistedSphinxInventory(
  176. logger=self.msg, project_name=self.projectname)
  177. def privacyClass(self, documentable):
  178. """
  179. Report the privacy level for an object.
  180. Hide all tests with the exception of L{twisted.test.proto_helpers}.
  181. param obj: Object for which the privacy is reported.
  182. type obj: C{model.Documentable}
  183. rtype: C{model.PrivacyClass} member
  184. """
  185. if documentable.fullName() == 'twisted.test':
  186. # Match this package exactly, so that proto_helpers
  187. # below is visible
  188. return model.PrivacyClass.VISIBLE
  189. current = documentable
  190. while current:
  191. if current.fullName() == 'twisted.test.proto_helpers':
  192. return model.PrivacyClass.VISIBLE
  193. if isinstance(current, model.Package) and current.name == 'test':
  194. return model.PrivacyClass.HIDDEN
  195. current = current.parent
  196. return super(TwistedSystem, self).privacyClass(documentable)