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()