From 5a2ca5f122ae356462b630464d0bfc863596e626 Mon Sep 17 00:00:00 2001
From: Jason Madden
Date: Thu, 18 May 2017 16:34:22 -0500
Subject: [PATCH] More coverage
---
src/zope/app/apidoc/browser/README.rst | 11 ++
src/zope/app/apidoc/browser/configure.zcml | 10 +-
src/zope/app/apidoc/browser/preference.py | 1 -
src/zope/app/apidoc/classregistry.py | 2 -
.../app/apidoc/codemodule/browser/README.rst | 103 +++++++++++++++++-
.../app/apidoc/codemodule/browser/class_.py | 14 +--
.../apidoc/codemodule/browser/introspector.py | 12 +-
.../app/apidoc/codemodule/browser/menu.py | 18 ++-
.../app/apidoc/codemodule/browser/tests.py | 64 +++++++++++
.../app/apidoc/codemodule/browser/text.py | 12 +-
.../app/apidoc/codemodule/browser/zcml.py | 6 +-
src/zope/app/apidoc/codemodule/class_.py | 5 +-
src/zope/app/apidoc/codemodule/module.py | 17 ++-
src/zope/app/apidoc/codemodule/tests.py | 9 ++
14 files changed, 223 insertions(+), 61 deletions(-)
diff --git a/src/zope/app/apidoc/browser/README.rst b/src/zope/app/apidoc/browser/README.rst
index 545a3470..675accc5 100644
--- a/src/zope/app/apidoc/browser/README.rst
+++ b/src/zope/app/apidoc/browser/README.rst
@@ -37,3 +37,14 @@ feel better and does not have all the O-wrap clutter:
zope-dev@zope.org.
...
+
+Preferences
+-----------
+
+The ``APIDOC`` skin also does the same for editing preference groups:
+
+ >>> browser.open('http://localhost/++preferences++apidoc/InterfaceDetails/apidocIndex.html')
+ >>> print(browser.contents)
+ <...
+ Preferences for API Docs' Interface Details Screen
+ ...
diff --git a/src/zope/app/apidoc/browser/configure.zcml b/src/zope/app/apidoc/browser/configure.zcml
index ec8c2a17..68cb8d84 100644
--- a/src/zope/app/apidoc/browser/configure.zcml
+++ b/src/zope/app/apidoc/browser/configure.zcml
@@ -5,14 +5,8 @@
i18n_domain="zope">
-
-
-
+
+
>> _context = traverse(cm, 'zope/app/apidoc/interfaces')
+ >>> details = browser.module.ModuleDetails(_context, TestRequest())
+ >>> pprint(details.getInterfaces())
+ [{'doc': 'Zope 3 API Documentation Module',
+ 'name': 'IDocumentationModule',
+ 'path': 'zope.app.apidoc.interfaces.IDocumentationModule',
+ 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/interfaces/IDocumentationModule'}]
+
+Module Details With Implementation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Let's also look at a module that implements an interface itself:
+
+ >>> _context = traverse(cm, 'zope/lifecycleevent')
+ >>> details = browser.module.ModuleDetails(_context, TestRequest())
+ >>> pprint(details.getModuleInterfaces())
+ [{'name': 'IZopeLifecycleEvent',
+ 'path': 'zope.lifecycleevent.interfaces.IZopeLifecycleEvent'}]
+
Class Details
-------------
@@ -284,8 +308,7 @@ ZCML files, just a template. The template then uses the directive details to
provide all the view content:
>>> details = browser.zcml.DirectiveDetails()
- >>> zcml = traverse(cm,
- ... 'zope/app/apidoc/codemodule/configure.zcml')
+ >>> zcml = traverse(cm, 'zope/app/apidoc/codemodule/configure.zcml')
>>> details.context = zcml.rootElement
>>> details.request = TestRequest()
>>> details.__parent__ = details.context
@@ -392,6 +415,82 @@ Returns a list of all sub-directives:
>>> details.getElements()
[]
+Other Examples
+~~~~~~~~~~~~~~
+
+Let's look at sub-directive that has a namespace:
+
+ >>> details = browser.zcml.DirectiveDetails()
+ >>> zcml = traverse(cm, 'zope/app/apidoc/ftesting-base.zcml')
+ >>> browser_directive = [x for x in zcml.rootElement.subs if x.name[0].endswith('browser')][0]
+ >>> details.context = browser_directive
+ >>> details.request = TestRequest()
+ >>> details.fullTagName()
+ 'browser:menu'
+
+The exact URL will vary depending on what ZCML has been loaded.
+
+ >>> details.url()
+ 'http://127.0.0.1/++apidoc++/.../menu/index.html'
+
+Now one that has some tokens:
+
+ >>> details = browser.zcml.DirectiveDetails()
+ >>> zcml = traverse(cm, 'zope/app/apidoc/enabled.zcml')
+ >>> adapter_directive = [x for x in zcml.rootElement.subs if x.name[1] == 'adapter'][0]
+ >>> details.context = adapter_directive
+ >>> details.__parent__ = details.context
+ >>> details.request = TestRequest()
+ >>> pprint(details.attributes())
+ [{'name': 'factory',
+ 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/apidoc/apidocNamespace/index.html',
+ 'value': '.apidoc.apidocNamespace',
+ 'values': []},
+ {'name': 'provides',
+ 'url': 'http://127.0.0.1/++apidoc++/Interface/zope.traversing.interfaces.ITraversable/index.html',
+ 'value': 'zope.traversing.interfaces.ITraversable',
+ 'values': []},
+ {'name': 'for', 'url': None, 'value': '*', 'values': []},
+ {'name': 'name', 'url': None, 'value': 'apidoc', 'values': []}]
+
+Now one with *multiple* tokens:
+
+ >>> details = browser.zcml.DirectiveDetails()
+ >>> zcml = traverse(cm, 'zope/traversing/configure.zcml')
+ >>> adapter_directive = [x for x in zcml.rootElement.subs if x.name[1] == 'adapter']
+ >>> adapter_directive = [x for x in adapter_directive if ' ' in x.attrs[(None, 'for')]][0]
+ >>> details.context = adapter_directive
+ >>> details.__parent__ = details.context
+ >>> details.request = TestRequest()
+ >>> pprint(details.attributes())
+ [{'name': 'factory',
+ 'url': 'http://127.0.0.1/++apidoc++/Code/zope/traversing/namespace/etc/index.html',
+ 'value': 'zope.traversing.namespace.etc',
+ 'values': []},
+ {'name': 'provides',
+ 'url': 'http://127.0.0.1/++apidoc++/Interface/zope.traversing.interfaces.ITraversable/index.html',
+ 'value': 'zope.traversing.interfaces.ITraversable',
+ 'values': []},
+ {'name': 'for',
+ 'url': None,
+ 'value': '* zope.publisher.interfaces.IRequest',
+ 'values': [{'url': None, 'value': '*'},
+ {'url': 'http://127.0.0.1/++apidoc++/Interface/zope.publisher.interfaces.IRequest/index.html',
+ 'value': 'zope.publisher.interfaces.IRequest'}]},
+ {'name': 'name', 'url': None, 'value': 'etc', 'values': []}]
+
+And now one that is subdirectives:
+
+ >>> details = browser.zcml.DirectiveDetails()
+ >>> zcml = traverse(cm, 'zope/app/apidoc/browser/configure.zcml')
+ >>> adapter_directive = [x for x in zcml.rootElement.subs if x.name[1] == 'pages'][0]
+ >>> details.context = adapter_directive.subs[0]
+ >>> details.__parent__ = details.context
+ >>> details.request = TestRequest()
+ >>> details.url()
+ 'http://127.0.0.1/++apidoc++/.../pages/index.html#page'
+
+
The Introspector
----------------
diff --git a/src/zope/app/apidoc/codemodule/browser/class_.py b/src/zope/app/apidoc/codemodule/browser/class_.py
index f253c0cb..9f88a900 100644
--- a/src/zope/app/apidoc/codemodule/browser/class_.py
+++ b/src/zope/app/apidoc/codemodule/browser/class_.py
@@ -155,11 +155,9 @@ def getDoc(self):
def getConstructor(self):
"""Get info about the constructor, or None if there isn't one."""
attr = self.context.getConstructor()
- if attr is None:
- return None
- attr = removeSecurityProxy(attr)
-
- return {
- 'signature': getFunctionSignature(attr, ignore_self=True),
- 'doc': renderText(attr.__doc__ or '', inspect.getmodule(attr)),
- }
+ if attr is not None:
+ attr = removeSecurityProxy(attr)
+ return {
+ 'signature': getFunctionSignature(attr, ignore_self=True),
+ 'doc': renderText(attr.__doc__ or '', inspect.getmodule(attr)),
+ }
diff --git a/src/zope/app/apidoc/codemodule/browser/introspector.py b/src/zope/app/apidoc/codemodule/browser/introspector.py
index cd460f5c..1d835827 100644
--- a/src/zope/app/apidoc/codemodule/browser/introspector.py
+++ b/src/zope/app/apidoc/codemodule/browser/introspector.py
@@ -49,7 +49,7 @@ def traverse(self, name, ignore):
# only available in dev-mode.
naked = zope.security.proxy.removeSecurityProxy(self.context)
annotations = annotation.interfaces.IAnnotations(naked)
- obj = name and annotations[name] or annotations
+ obj = annotations[name] if name else annotations
if not IPhysicallyLocatable(obj, False):
obj = location.LocationProxy(
obj, self.context, '++annotations++'+name)
@@ -168,10 +168,8 @@ def getMethods(self):
val = getattr(obj, name)
if not (inspect.ismethod(val) or inspect.ismethoddescriptor(val)):
continue
- if inspect.ismethod(val):
- signature = apidoc.utilities.getFunctionSignature(val)
- else:
- signature = '(...)'
+
+ signature = apidoc.utilities.getFunctionSignature(val)
entry = {
'name': name,
@@ -231,8 +229,6 @@ def getAnnotationsInfo(self):
# so we want to see things that we usually cannot see
naked = zope.security.proxy.removeSecurityProxy(self.context)
annotations = annotation.interfaces.IAnnotations(naked)
- if not hasattr(annotations, 'items'):
- return
ann = []
for key, value in annotations.items():
ann.append({
@@ -241,5 +237,5 @@ def getAnnotationsInfo(self):
'value': repr(value),
'value_type': type(value).__name__,
'value_type_link': getTypeLink(type(value))
- })
+ })
return ann
diff --git a/src/zope/app/apidoc/codemodule/browser/menu.py b/src/zope/app/apidoc/codemodule/browser/menu.py
index 2a858280..1ec93a23 100644
--- a/src/zope/app/apidoc/codemodule/browser/menu.py
+++ b/src/zope/app/apidoc/codemodule/browser/menu.py
@@ -70,11 +70,9 @@ def findClasses(self):
return []
classModule = traverse(self.context, '/++apidoc++')['Code']
classModule.setup()
+ found = [p for p in classRegistry if path in p]
results = []
- for p in classRegistry:
- if path not in p:
- continue
-
+ for p in found:
klass = traverse(classModule, p.replace('.', '/'))
results.append({
'path': p,
@@ -115,13 +113,13 @@ def findAllClasses(self):
results = []
counter = 0
- for p in classRegistry.keys():
+ for p in list(classRegistry): # Traversing can potentially change the registry
klass = traverse(classModule, p.replace('.', '/'))
- results.append(
- {'path': p,
- 'url': absoluteURL(klass, self.request),
- 'counter': counter
- })
+ results.append({
+ 'path': p,
+ 'url': absoluteURL(klass, self.request),
+ 'counter': counter
+ })
counter += 1
results.sort(key=_pathgetter)
diff --git a/src/zope/app/apidoc/codemodule/browser/tests.py b/src/zope/app/apidoc/codemodule/browser/tests.py
index 06f4864c..da2363f6 100644
--- a/src/zope/app/apidoc/codemodule/browser/tests.py
+++ b/src/zope/app/apidoc/codemodule/browser/tests.py
@@ -105,6 +105,70 @@ def testZCMLFileDetailsView(self):
body, '/++apidoc++/Code/zope/app/apidoc/configure.zcml/index.html',
basic='mgr:mgrpw')
+from BTrees import OOBTree
+class TestClass(unittest.TestCase):
+
+ layer = APIDocLayer
+
+ @unittest.skipIf(OOBTree.OOBTree is OOBTree.OOBTreePy,
+ "Only in the C implementation")
+ def test_listClasses_C(self):
+ from zope.app.apidoc.codemodule.browser.class_ import ClassDetails
+ from zope.publisher.browser import TestRequest
+ # BTree items doesn't set a module.
+
+ items_class = type(OOBTree.OOBTree({1: 2}).items())
+
+ details = ClassDetails()
+ details.request = TestRequest()
+ details.context = self.layer.getRootFolder()
+
+ info = details._listClasses([items_class])
+ self.assertIsNone(info[0]['url'], None)
+
+class TestIntrospectorNS(unittest.TestCase):
+
+ def _check_namespace(self, kind, context, name):
+ from zope.app.apidoc.codemodule.browser import introspector
+ from zope.location import LocationProxy
+
+ namespace = getattr(introspector, kind + 'Namespace')
+ traverser = namespace(context)
+ obj = traverser.traverse(name, ())
+ self.assertIsInstance(obj, LocationProxy)
+
+ self.assertIs(obj.__parent__, context)
+ self.assertTrue(obj.__name__.endswith(name))
+ return traverser, obj
+
+ def test_annotations(self):
+ from zope.annotation.attribute import AttributeAnnotations
+ anot = AttributeAnnotations(self)
+ anot['key'] = 42
+ self._check_namespace('annotations', anot, 'key')
+
+ def test_items(self):
+ self._check_namespace('sequenceItems', ["value"], '0')
+
+ def test_mapping(self):
+ self._check_namespace('mappingItems', {'key': 'value'}, 'key')
+
+
+class TestIntrospector(unittest.TestCase):
+ layer = APIDocLayer
+
+ classAttr = 1
+
+ def testIntrospector(self):
+ from zope.app.apidoc.codemodule.browser.introspector import Introspector
+ from zope.publisher.browser import TestRequest
+
+ ispect = Introspector(self, TestRequest())
+ atts = list(ispect.getAttributes())
+ names = [x['name'] for x in atts]
+ self.assertIn('classAttr', names)
+ self.assertNotIn('testAttributes', names)
+
def test_suite():
return unittest.TestSuite((
diff --git a/src/zope/app/apidoc/codemodule/browser/text.py b/src/zope/app/apidoc/codemodule/browser/text.py
index bdf2d327..c9db885f 100644
--- a/src/zope/app/apidoc/codemodule/browser/text.py
+++ b/src/zope/app/apidoc/codemodule/browser/text.py
@@ -13,7 +13,6 @@
##############################################################################
"""Function Views
-$Id$
"""
__docformat__ = 'restructuredtext'
from zope.app.apidoc.utilities import renderText
@@ -21,10 +20,11 @@
class TextFileDetails(object):
"""Represents the details of the text file."""
+ context = None
+ request = None
+
def renderedContent(self):
"""Render the file content to HTML."""
- if self.context.path.endswith('.stx'):
- format = 'zope.source.stx'
- else:
- format = 'zope.source.rest'
- return renderText(self.context.getContent(), format=format)
+ ctx = self.context
+ format = 'zope.source.stx' if ctx.path.endswith('.stx') else 'zope.source.rest'
+ return renderText(ctx.getContent(), format=format)
diff --git a/src/zope/app/apidoc/codemodule/browser/zcml.py b/src/zope/app/apidoc/codemodule/browser/zcml.py
index a29cfdf8..7c3393c0 100644
--- a/src/zope/app/apidoc/codemodule/browser/zcml.py
+++ b/src/zope/app/apidoc/codemodule/browser/zcml.py
@@ -93,12 +93,12 @@ def objectURL(self, value, field, rootURL):
return
try:
isInterface = IInterface.providedBy(obj)
- except (AttributeError, TypeError):
+ except (AttributeError, TypeError): # pragma: no cover
# probably an object that does not like to play nice with the CA
isInterface = False
# The object might be an instance; in this case get a link to the class
- if not hasattr(obj, '__name__'):
+ if not hasattr(obj, '__name__'): # pragma: no cover
obj = getattr(obj, '__class__')
path = getPythonPath(obj)
if isInterface:
@@ -133,7 +133,7 @@ def attributes(self):
if isinstance(field,
(GlobalObject, GlobalInterface)):
url = self.objectURL(value, field, rootURL)
- else:
+ else: # pragma: no cover
break
attr['values'].append({'value': value, 'url': url})
diff --git a/src/zope/app/apidoc/codemodule/class_.py b/src/zope/app/apidoc/codemodule/class_.py
index 5a3d8a53..3584d642 100644
--- a/src/zope/app/apidoc/codemodule/class_.py
+++ b/src/zope/app/apidoc/codemodule/class_.py
@@ -107,6 +107,5 @@ def getSecurityChecker(self):
def getConstructor(self):
"""See IClassDocumentation."""
init = getattr(self.__klass, '__init__', None)
- if not callable(init):
- return None
- return init
+ if callable(init):
+ return init
diff --git a/src/zope/app/apidoc/codemodule/module.py b/src/zope/app/apidoc/codemodule/module.py
index 8d468c3a..1a12bda4 100644
--- a/src/zope/app/apidoc/codemodule/module.py
+++ b/src/zope/app/apidoc/codemodule/module.py
@@ -65,8 +65,9 @@ def __init__(self, parent, name, module, setup=True):
self.__parent__ = parent
self.__name__ = name
self._module = module
- self.__needsSetup = setup
- self.__setup()
+ self._children = {}
+ if setup:
+ self.__setup()
def __setup_package(self):
# Detect packages
@@ -131,6 +132,8 @@ def __setup_classes_and_functions(self):
# imported from other modules.
names = set()
for name, attr in self._module.__dict__.items():
+ if isinstance(attr, hookable):
+ attr = attr.implementation
attr_module = getattr(attr, '__module__', None)
if attr_module != self._module.__name__:
continue
@@ -160,17 +163,11 @@ def __setup_classes_and_functions(self):
doc = attr.__doc__
if not doc:
f = module_decl.get(name)
- if f is not None:
- doc = f.__doc__
+ doc = getattr(f, '__doc__', None)
self._children[name] = Function(self, name, attr, doc=doc)
def __setup(self):
"""Setup the module sub-tree."""
- if not self.__needsSetup:
- return
-
- self.__needsSetup = False
- self._children = {}
self.__setup_package()
zope.deprecation.__show__.off()
@@ -181,7 +178,7 @@ def __setup(self):
def withParentAndName(self, parent, name):
located = type(self)(parent, name, self._module, False)
- new_children = located._children = {}
+ new_children = located._children
for x in self._children.values():
try:
new_child = x.withParentAndName(located, x.__name__)
diff --git a/src/zope/app/apidoc/codemodule/tests.py b/src/zope/app/apidoc/codemodule/tests.py
index f612813f..92e3e7e7 100644
--- a/src/zope/app/apidoc/codemodule/tests.py
+++ b/src/zope/app/apidoc/codemodule/tests.py
@@ -69,6 +69,7 @@ def test_idempotent(self):
before = len(mod)
self.assertGreater(before, 0)
self.assertTrue(mod._package)
+ mod._Module__needsSetup = True
mod._Module__setup()
self.assertEqual(len(mod), before)
@@ -87,6 +88,14 @@ def test_access_attributes(self):
self.assertEqual(here, mod['here'])
self.assertEqual(mod['here'].__name__, 'here')
+ def test_hookable(self):
+ import zope.component._api
+ from zope.hookable import hookable
+ self.assertIsInstance(zope.component._api.getSiteManager, hookable)
+ from zope.app.apidoc.codemodule.function import Function
+ mod = Module(None, 'hooks', zope.component._api)
+ self.assertIsInstance(mod._children['getSiteManager'], Function)
+
def test_suite():
checker = standard_checker()