diff --git a/CHANGES.rst b/CHANGES.rst index 185a9550..0de6f989 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,10 @@ CHANGES - Add support for Python 3.4, 3.5, 3.6 and PyPy. +- The ``++apidoc++`` namespace is now always located beneath the root. + This solves issues with multiple threads and concurrent requests and + also circular parent chains. + - Modernize some of the templates. An increment towards having zope.app.apidoc compatible with Chameleon. diff --git a/src/zope/app/apidoc/README.rst b/src/zope/app/apidoc/README.rst index daa9b915..b66072e3 100644 --- a/src/zope/app/apidoc/README.rst +++ b/src/zope/app/apidoc/README.rst @@ -47,13 +47,12 @@ Now we can instantiate the class (which is usually done when traversing >>> from zope.app.apidoc.apidoc import APIDocumentation >>> doc = APIDocumentation(None, '++apidoc++') - >>> modules = doc.keys() - >>> modules.sort() + >>> modules = sorted(doc.keys()) >>> modules [u'Interface', u'ZCML'] >>> doc['ZCML'] - + Developing a Module diff --git a/src/zope/app/apidoc/apidoc.py b/src/zope/app/apidoc/apidoc.py index c6166be1..fef24b49 100644 --- a/src/zope/app/apidoc/apidoc.py +++ b/src/zope/app/apidoc/apidoc.py @@ -14,7 +14,6 @@ """Zope 3 API Documentation """ -from __future__ import absolute_import __docformat__ = 'restructuredtext' import zope.component @@ -39,21 +38,31 @@ def __init__(self, parent, name): self.__parent__ = parent self.__name__ = name + def __locate(self, obj, name): + # In general, *always* doing this is not just weird (threads), it also leads to + # circular __parent__ chains, which causes issues with things like + # zope.securitypolicy + # (https://github.com/zopefoundation/zope.securitypolicy/issues/8), and + # it can lead to bad behaviour with sharing persistent objects + # across ZODB connections (which may close). Fortunately, our traverser + # takes care of this by making sure we are always located at an (equivalent) + # fresh root. + locate(obj, self, name) + return obj + def get(self, key, default=None): """See zope.container.interfaces.IReadContainer""" utility = zope.component.queryUtility(IDocumentationModule, key, default) - if utility != default: - locate(utility, self, key) + if utility is not default: + utility = self.__locate(utility, key) return utility def items(self): """See zope.container.interfaces.IReadContainer""" - items = list(zope.component.getUtilitiesFor(IDocumentationModule)) - items.sort() + items = sorted(zope.component.getUtilitiesFor(IDocumentationModule)) utils = [] for key, value in items: - locate(value, self, key) - utils.append((key, value)) + utils.append((key, self.__locate(value, key))) return utils @@ -70,4 +79,12 @@ def traverse(self, name, ignore): def handleNamespace(ob, name): """Used to traverse to an API Documentation.""" - return APIDocumentation(ob, '++apidoc++'+name) + # Ignore the `ob` we traverse through. We always want to be + # located at the root, although not the *actual* root. + # This is because we have to reparent our children, which are + # shared, in-memory utilities, and in the presence of multiple threads, + # doing so at different times would be bad in case connections get closed. + # So we make a pseudo root. + from zope.site.folder import rootFolder + ob = rootFolder() + return APIDocumentation(ob, '++apidoc++' + name) diff --git a/src/zope/app/apidoc/codemodule/README.rst b/src/zope/app/apidoc/codemodule/README.rst index e0c49d43..7de935b5 100644 --- a/src/zope/app/apidoc/codemodule/README.rst +++ b/src/zope/app/apidoc/codemodule/README.rst @@ -89,7 +89,7 @@ still get to them: >>> names = sorted(module['tests'].keys()) >>> names - ['BrowserTestCase', 'LayerDocFileSuite', 'LayerDocTestSuite', 'Root', 'rootLocation', 'setUp', 'standard_checker', 'tearDown', 'test_suite'] + ['BrowserTestCase', 'LayerDocFileSuite', 'LayerDocTestSuite', 'Root', 'setUp', 'standard_checker', 'tearDown', 'test_suite'] Classes diff --git a/src/zope/app/apidoc/codemodule/browser/README.rst b/src/zope/app/apidoc/codemodule/browser/README.rst index 29424241..7f33861e 100644 --- a/src/zope/app/apidoc/codemodule/browser/README.rst +++ b/src/zope/app/apidoc/codemodule/browser/README.rst @@ -44,7 +44,7 @@ Return info objects for all classes in this module. >>> pprint(details.getClasses()) [{'doc': 'Represent the code browser documentation root', 'name': 'CodeModule', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/codemodule/codemodule/CodeModule'}] + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule/codemodule/CodeModule'}] This module doesn't contain anything else. @@ -75,17 +75,17 @@ way up to the root, but we just want to go to the root module. >>> bc.request = details.request >>> pprint(bc(), width=1) [{'name': u'[top]', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code'}, + 'url': 'http://127.0.0.1/++apidoc++/Code'}, {'name': u'zope', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope'}, + 'url': 'http://127.0.0.1/++apidoc++/Code/zope'}, {'name': 'app', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app'}, + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app'}, {'name': 'apidoc', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc'}, + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc'}, {'name': 'codemodule', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/codemodule'}, + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule'}, {'name': 'codemodule', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/codemodule/codemodule'}] + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule/codemodule'}] Class Details @@ -109,7 +109,7 @@ Get all bases of this class. >>> pprint(details.getBases()) [{'path': 'zope.app.apidoc.codemodule.module.Module', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/codemodule/module/Module'}] + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule/module/Module'}] `getKnownSubclasses()` ~~~~~~~~~~~~~~~~~~~~~~ @@ -130,9 +130,9 @@ Prepare a list of classes for presentation. ... zope.app.apidoc.apidoc.APIDocumentation, ... zope.app.apidoc.codemodule.codemodule.Module])) [{'path': 'zope.app.apidoc.apidoc.APIDocumentation', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/apidoc/APIDocumentation'}, + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/apidoc/APIDocumentation'}, {'path': 'zope.app.apidoc.codemodule.module.Module', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/codemodule/module/Module'}] + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule/module/Module'}] `getBaseURL()` ~~~~~~~~~~~~~~ @@ -143,7 +143,7 @@ Note that the following output is a bit different than usual, since we have not setup all path elements. >>> details.getBaseURL() - 'http://127.0.0.1/++etc++site/++apidoc++' + 'http://127.0.0.1/++apidoc++' `getInterfaces()` ~~~~~~~~~~~~~~~~~ @@ -251,7 +251,7 @@ Get all attributes of this function. Return the URL for the API Documentation Tool. >>> details.getBaseURL() - 'http://127.0.0.1/++etc++site/++apidoc++' + 'http://127.0.0.1/++apidoc++' Text File Details @@ -330,7 +330,7 @@ Returns the URL of the directive docuemntation in the ZCML documentation module. >>> details.url() - u'http://127.0.0.1/++etc++site/++apidoc++/ZCML/ALL/configure/index.html' + u'http://127.0.0.1/++apidoc++/ZCML/ALL/configure/index.html' `objectURL(value, field, rootURL)` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -342,7 +342,7 @@ crafts a documentation URL for it: >>> field = GlobalObject() >>> details.objectURL('.interfaces.IZCMLFile', field, '') - 'http://127.0.0.1/++etc++site/++apidoc++/Interface/zope.app.apidoc.codemodule.interfaces.IZCMLFile/index.html' + 'http://127.0.0.1/++apidoc++/Interface/zope.app.apidoc.codemodule.interfaces.IZCMLFile/index.html' >>> details.objectURL('.zcml.ZCMLFile', field, '') '/zope/app/apidoc/codemodule/zcml/ZCMLFile/index.html' @@ -371,7 +371,7 @@ will be listed too. >>> details.context = details.context.subs[0] >>> pprint(details.attributes()) [{'name': u'class', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/codemodule/module/Module/index.html', + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule/module/Module/index.html', 'value': u'.module.Module', 'values': []}] @@ -481,14 +481,14 @@ class. In the following section we are going to demonstrate the methods used to collect the data. First we need to create an object though; let's use a root folder: - >>> rootFolder.__parent__ + >>> rootFolder Now we instantiate the view >>> from zope.publisher.browser import TestRequest >>> request = TestRequest() - >>> inspect = introspector.Introspector(rootFolder.__parent__, request) + >>> inspect = introspector.Introspector(rootFolder, request) so that we can start looking at the methods. First we should note that the class documentation view is directly available: @@ -585,7 +585,7 @@ and then get the sequence items: Similar functionality exists for a mapping. But we first have to add an item: - >>> rootFolder.__parent__['list'] = list + >>> rootFolder['list'] = list Now let's have a look: @@ -609,7 +609,7 @@ object is annotatable, then we can get an annotation mapping: - >>> rootFolder.__parent__.__annotations__ = {'my.list': list} + >>> rootFolder.__annotations__ = {'my.list': list} >>> pprint(inspect.getAnnotationsInfo()) [{'key': 'my.list', diff --git a/src/zope/app/apidoc/codemodule/browser/class_.py b/src/zope/app/apidoc/codemodule/browser/class_.py index 7a2b9420..10b76c73 100644 --- a/src/zope/app/apidoc/codemodule/browser/class_.py +++ b/src/zope/app/apidoc/codemodule/browser/class_.py @@ -31,8 +31,8 @@ from zope.app.apidoc.utilities import isReferencable -def getTypeLink(type): - if type is type(None): +def getTypeLink(type, _NoneType=type(None)): + if type is _NoneType: return None path = getPythonPath(type) return path.replace('.', '/') if isReferencable(path) else None @@ -157,7 +157,8 @@ def getConstructor(self): if attr is None: return 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/menu.py b/src/zope/app/apidoc/codemodule/browser/menu.py index bc4b8d11..4d763f26 100644 --- a/src/zope/app/apidoc/codemodule/browser/menu.py +++ b/src/zope/app/apidoc/codemodule/browser/menu.py @@ -65,22 +65,22 @@ def findClasses(self): >>> from pprint import pprint >>> pprint(info) [{'path': 'zope.app.apidoc.codemodule.browser.Foo', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/codemodule/browser/Foo/'}, + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule/browser/Foo/'}, {'path': 'zope.app.apidoc.codemodule.browser.Foo2', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/codemodule/browser/Foo2/'}] + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule/browser/Foo2/'}] >>> menu.request = TestRequest(form={'path': 'o2'}) >>> info = menu.findClasses() >>> pprint(info) [{'path': 'zope.app.apidoc.codemodule.browser.Foo2', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/codemodule/browser/Foo2/'}] + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule/browser/Foo2/'}] >>> menu.request = TestRequest(form={'path': 'Blah'}) >>> info = menu.findClasses() >>> pprint(info) [{'path': 'zope.app.apidoc.codemodule.browser.Blah', - 'url': 'http://127.0.0.1/++etc++site/++apidoc++/Code/zope/app/apidoc/codemodule/browser/Blah/'}] + 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule/browser/Blah/'}] """ path = self.request.get('path', None) diff --git a/src/zope/app/apidoc/interfaces.py b/src/zope/app/apidoc/interfaces.py index 80988e31..6f8a99cf 100644 --- a/src/zope/app/apidoc/interfaces.py +++ b/src/zope/app/apidoc/interfaces.py @@ -17,8 +17,9 @@ from zope.interface import Interface from zope.schema import TextLine, Text +from zope.location.interfaces import ILocation -class IDocumentationModule(Interface): +class IDocumentationModule(ILocation): """Zope 3 API Documentation Module A documentation module contains the documentation for one specific aspect diff --git a/src/zope/app/apidoc/static.py b/src/zope/app/apidoc/static.py index 1174a2fa..3f7fe044 100644 --- a/src/zope/app/apidoc/static.py +++ b/src/zope/app/apidoc/static.py @@ -264,7 +264,7 @@ def processLink(self, link): # Retrieve the content try: self.browser.open(link.callableURL) - except urllib2.HTTPError, error: + except urllib2.HTTPError as error: # Something went wrong with retrieving the page. self.linkErrors += 1 self.sendMessage( @@ -279,7 +279,7 @@ def processLink(self, link): self.sendMessage('Bad URL: ' + link.callableURL, 2) self.sendMessage('+-> Reference: ' + link.referenceURL, 2) return - except Exception, error: + except Exception as error: # This should never happen outside the debug mode. We really want # to catch all exceptions, so that we can investigate them. if self.options.debug: @@ -307,7 +307,7 @@ def processLink(self, link): try: links = self.browser.links() - except HTMLParser.HTMLParseError, error: + except HTMLParser.HTMLParseError as error: self.htmlErrors += 1 self.sendMessage('Failed to parse HTML: ' + url, 1) self.sendMessage('+-> %s: line %i, column %s' % ( diff --git a/src/zope/app/apidoc/tests.py b/src/zope/app/apidoc/tests.py index f6095ebe..b7d75fd8 100644 --- a/src/zope/app/apidoc/tests.py +++ b/src/zope/app/apidoc/tests.py @@ -14,19 +14,13 @@ """Tests for the Interface Documentation Module """ -import re +import doctest import os +import re import unittest -import doctest +import zope.app.renderer import zope.component.testing -from zope.configuration import xmlconfig -from zope.interface import implementer -from zope.traversing.interfaces import IContainmentRoot -from zope.location import LocationProxy - -from zope.testing import renormalizing - import zope.testing.module from webtest import TestApp @@ -34,7 +28,13 @@ from zope.app.apidoc.apidoc import APIDocumentation from zope.app.apidoc.testing import APIDocLayer -from zope.app.component.testing import PlacefulSetup, setUpTraversal +from zope.app.component.testing import PlacefulSetup +from zope.app.component.testing import setUpTraversal + +from zope.configuration import xmlconfig +from zope.interface import implementer +from zope.testing import renormalizing +from zope.traversing.interfaces import IContainmentRoot _old_appsetup_context = None @@ -58,7 +58,10 @@ def _tearDown_AppSetup(): def _setUp_LayerPlace(test): # We're in the layer, so we don't want to tear down zope.testing, # which would tear down zope.component - root_folder = PlacefulSetup().buildFolders(True) + psetup = PlacefulSetup() + # Make a folder tree and a site. + psetup.buildFolders(True) + root_folder = psetup.rootFolder setUpTraversal() global _old_appsetup_context @@ -77,6 +80,8 @@ def _tearDown_LayerPlace(test): class BrowserTestCase(unittest.TestCase): + layer = APIDocLayer + def setUp(self): super(BrowserTestCase, self).setUp() _setUp_AppSetup() @@ -91,23 +96,25 @@ def checkForBrokenLinks(self, orig_response, path, basic=None): response = self.publish(path, basic=basic) links = response.html.find_all('a') + seen = set() for link in links: try: href = link.attrs['href'] except KeyError: pass - if '++apidoc++' in href: - # XXX: We are this! Enable it - # We don't install this at test time - continue - if href.startswith('http://dev.zope.org'): - # Don't try to follow external links - continue + if href.startswith('./'): href = href[2:] + if href.startswith('http://localhost/'): + href = href[16:] + if not href.startswith('/'): href = path.rsplit('/', 1)[0] + '/' + href + + if href in seen: + continue + seen.add(href) self.publish(href, basic=basic) def publish(self, path, basic=None, form=None, headers=None, env=None): @@ -129,7 +136,6 @@ def publish(self, path, basic=None, form=None, headers=None, env=None): def setUp(test): - import zope.app.renderer zope.component.testing.setUp() xmlconfig.file('configure.zcml', zope.app.renderer) zope.testing.module.setUp(test, 'zope.app.apidoc.doctest') @@ -147,9 +153,6 @@ class Root(object): __parent__ = None __name__ = '' -def rootLocation(obj, name): - return LocationProxy(obj, Root(), name) - standard_checker_patterns = ( (re.compile(r"u('[^']*')"), r"\1"), (re.compile(r"b('[^']*')"), r"\1"), diff --git a/src/zope/app/apidoc/typemodule/type.py b/src/zope/app/apidoc/typemodule/type.py index 80769f3c..4685f4f8 100644 --- a/src/zope/app/apidoc/typemodule/type.py +++ b/src/zope/app/apidoc/typemodule/type.py @@ -38,7 +38,8 @@ class TypeInterface(ReadContainerBase): ... pass >>> @implementer(IFoo) ... class Foo(object): - ... pass + ... def getName(self): + ... return 'IFoo' >>> from zope import component as ztapi >>> ztapi.provideUtility(Foo(), IFoo, 'Foo') @@ -69,7 +70,7 @@ def items(self): """See zope.container.interfaces.IReadContainer""" results = [(name, LocationProxy(iface, self, name)) for name, iface in getUtilitiesFor(self.interface)] - results.sort(key=lambda x: x[1].__name__) + results.sort(key=lambda x: (x[1].getName(), x[0])) return results @@ -106,6 +107,9 @@ class TypeModule(ReadContainerBase): all content type interfaces, for example. """) + __name__ = None + __parent__ = None + def get(self, key, default=None): return TypeInterface( queryUtility(IInterface, key, default=default), self, key) diff --git a/src/zope/app/apidoc/utilities.py b/src/zope/app/apidoc/utilities.py index 722b08d0..dc97974e 100644 --- a/src/zope/app/apidoc/utilities.py +++ b/src/zope/app/apidoc/utilities.py @@ -72,6 +72,12 @@ def truncateSysPath(path): class ReadContainerBase(object): """Base for `IReadContainer` objects.""" + def __repr__(self): + if getattr(self, '__name__', None) is None: + return super(ReadContainerBase, self).__repr__() + c = type(self) + return "<%s.%s '%s' at 0x%x>" % (c.__module__, c.__name__, self.__name__, id(self)) + def get(self, key, default=None): raise NotImplementedError() @@ -193,10 +199,18 @@ def getPermissionIds(name, checker=_marker, klass=_marker): def getFunctionSignature(func, ignore_self=False): """Return the signature of a function or method.""" - if not isinstance(func, (types.FunctionType, types.MethodType)): - raise TypeError("func must be a function or method") + if not callable(func): #isinstance(func, (types.FunctionType, types.MethodType)): + raise TypeError("func must be a function or method not a %s (%r)" % (type(func), func)) + + try: + args, varargs, varkw, defaults = inspect.getargspec(func) + except TypeError: + # On Python 2, inspect.getargspec can't handle certain types + # of callable things, like object.__init__ ( is not + # a python function), but it *can* handle them on Python 3. + # Punt on Python 2 + return '()' - args, varargs, varkw, defaults = inspect.getargspec(func) placeholder = object() sig = '(' # By filling up the default tuple, we now have equal indeces for args and diff --git a/src/zope/app/apidoc/utilities.rst b/src/zope/app/apidoc/utilities.rst index d0bd7ad8..573dc6a3 100644 --- a/src/zope/app/apidoc/utilities.rst +++ b/src/zope/app/apidoc/utilities.rst @@ -434,7 +434,7 @@ If you do not pass a function or method to the function, it will fail: >>> utilities.getFunctionSignature('func') Traceback (most recent call last): ... - TypeError: func must be a function or method + TypeError: func must be a function or method not a ... A very uncommon, but perfectly valid (in Python 2), case is that tuple arguments are unpacked inside the argument list of the function. Here is an example: diff --git a/src/zope/app/apidoc/utilitymodule/browser.py b/src/zope/app/apidoc/utilitymodule/browser.py index 001426c0..b8cf6d29 100644 --- a/src/zope/app/apidoc/utilitymodule/browser.py +++ b/src/zope/app/apidoc/utilitymodule/browser.py @@ -31,6 +31,9 @@ class UtilityDetails(object): """Utility Details View.""" + context = None + request = None + def getAPIDocRootURL(self): return findAPIDocumentationRootURL(self.context, self.request) @@ -60,6 +63,9 @@ def getComponent(self): class Menu(object): """Menu View Helper Class""" + context = None + request = None + def getMenuTitle(self, node): """Return the title of the node that is displayed in the menu.""" obj = node.context @@ -78,4 +84,3 @@ def getMenuLink(self, node): return '%s/Utility/%s/%s/index.html' % (apidoc_url, getName(iface), getName(obj)) if isinstance(obj, UtilityInterface): return '%s/Interface/%s/index.html' % (apidoc_url, getName(obj)) - return None diff --git a/src/zope/app/apidoc/utilitymodule/browser.rst b/src/zope/app/apidoc/utilitymodule/browser.rst index 5ec8eb57..8cb11d6e 100644 --- a/src/zope/app/apidoc/utilitymodule/browser.rst +++ b/src/zope/app/apidoc/utilitymodule/browser.rst @@ -40,7 +40,7 @@ You can now get the title and link from the menu: >>> menu.getMenuTitle(node) 'iface' >>> menu.getMenuLink(node) - 'http://127.0.0.1/++etc++site/++apidoc++/Interface/foo.bar.iface/index.html' + 'http://127.0.0.1/++apidoc++/Interface/foo.bar.iface/index.html' Next, let's get the menu title and link for a utility with a name. We first have to create a utility registration @@ -61,7 +61,7 @@ We can now ask the menu to give us the tile and link for the utility: >>> menu.getMenuTitle(node) 'FooBar' >>> menu.getMenuLink(node) - 'http://127.0.0.1/++etc++site/++apidoc++/Utility/foo.bar.iface/Rm9vQmFy/index.html' + 'http://127.0.0.1/++apidoc++/Utility/foo.bar.iface/Rm9vQmFy/index.html' Finally, we get menu title and link for a utility without a name: @@ -76,7 +76,7 @@ Finally, we get menu title and link for a utility without a name: >>> menu.getMenuTitle(node) 'no name' >>> menu.getMenuLink(node) - 'http://127.0.0.1/++etc++site/++apidoc++/Utility/foo.bar.iface/X19ub25hbWVfXw==/index.html' + 'http://127.0.0.1/++apidoc++/Utility/foo.bar.iface/X19ub25hbWVfXw==/index.html' `UtilityDetails` class diff --git a/src/zope/app/apidoc/utilitymodule/utilitymodule.py b/src/zope/app/apidoc/utilitymodule/utilitymodule.py index 3c999aa9..be60dbce 100644 --- a/src/zope/app/apidoc/utilitymodule/utilitymodule.py +++ b/src/zope/app/apidoc/utilitymodule/utilitymodule.py @@ -117,6 +117,9 @@ class UtilityModule(ReadContainerBase): methods the utility provides and provides a link to the implementation. """) + __name__ = None + __parent__ = None + def get(self, key, default=None): parts = key.split('.') try: @@ -135,7 +138,7 @@ def items(self): # Get the next site manager sm = smlist.pop() # If we have already looked at this site manager, then skip it - if sm in seen: + if sm in seen: # pragma: no cover continue # Add the current site manager to the list of seen ones seen.append(sm) diff --git a/src/zope/app/apidoc/zcmlmodule/README.rst b/src/zope/app/apidoc/zcmlmodule/README.rst index f0725d19..1cafdc35 100644 --- a/src/zope/app/apidoc/zcmlmodule/README.rst +++ b/src/zope/app/apidoc/zcmlmodule/README.rst @@ -56,8 +56,8 @@ The namespace manages a particular ZCML namespace. The object always expects the parent to be a `ZCMLModule` instance. So let's create a namespace: >>> module = ZCMLModule() - >>> module._makeDocStructure() - >>> from zope.app.apidoc.zcmlmodule import Namespace + >>> from zope.app.apidoc.zcmlmodule import Namespace, _clear + >>> _clear() >>> ns = Namespace(ZCMLModule(), 'http://namespaces.zope.org/browser') We can now get its short name, which is the name without the URL prefix: diff --git a/src/zope/app/apidoc/zcmlmodule/__init__.py b/src/zope/app/apidoc/zcmlmodule/__init__.py index ea805f24..9a71652c 100644 --- a/src/zope/app/apidoc/zcmlmodule/__init__.py +++ b/src/zope/app/apidoc/zcmlmodule/__init__.py @@ -76,6 +76,7 @@ def getQuotedName(self): def get(self, key, default=None): """See zope.container.interfaces.IReadContainer""" + _makeDocStructure() ns = self.getFullName() if key not in namespaces[ns]: return default @@ -86,11 +87,10 @@ def get(self, key, default=None): def items(self): """See zope.container.interfaces.IReadContainer""" - list = [] - for key in namespaces[self.getFullName()].keys(): - list.append((key, self.get(key))) - list.sort() - return list + _makeDocStructure() + return sorted(((key, self.get(key)) + for key + in namespaces[self.getFullName()].keys())) @implementer(ILocation) @@ -130,26 +130,15 @@ class ZCMLModule(ReadContainerBase): available attributes. """) - def _makeDocStructure(self): - # Some trivial caching - global namespaces - global subdirs - context = zope.app.appsetup.appsetup.getConfigContext() - namespaces, subdirs = docutils.makeDocStructures(context) - - # Empty keys are not so good for a container - if '' in namespaces: - namespaces['ALL'] = namespaces[''] - del namespaces[''] - + __name__ = None + __parent__ = None def get(self, key, default=None): """See zope.container.interfaces.IReadContainer Get the namespace by name; long and abbreviated names work. """ - if namespaces is None or subdirs is None: - self._makeDocStructure() + _makeDocStructure() key = unquoteNS(key) if key in namespaces: @@ -164,16 +153,30 @@ def get(self, key, default=None): def items(self): """See zope.container.interfaces.IReadContainer""" - if namespaces is None or subdirs is None: - self._makeDocStructure() - list = [] + _makeDocStructure() + result = [] for key in namespaces: namespace = Namespace(self, key) # We need to make sure that we use the quoted URL as key - list.append((namespace.getQuotedName(), namespace)) - list.sort() - return list + result.append((namespace.getQuotedName(), namespace)) + result.sort() + return result + + +def _makeDocStructure(): + # Some trivial caching + global namespaces + global subdirs + if namespaces is not None and subdirs is not None: + return + + context = zope.app.appsetup.appsetup.getConfigContext() + namespaces, subdirs = docutils.makeDocStructures(context) + # Empty keys are not so good for a container + if '' in namespaces: + namespaces['ALL'] = namespaces[''] + del namespaces[''] def _clear(): global namespaces diff --git a/src/zope/app/apidoc/zcmlmodule/browser.py b/src/zope/app/apidoc/zcmlmodule/browser.py index b61a4da1..be9b147a 100644 --- a/src/zope/app/apidoc/zcmlmodule/browser.py +++ b/src/zope/app/apidoc/zcmlmodule/browser.py @@ -102,7 +102,6 @@ def getFileInfo(self): 'column': info.column, 'eline': info.eline, 'ecolumn': info.ecolumn} - return None def getInfo(self): """Get the file where the directive was declared.""" @@ -117,7 +116,6 @@ def getHandler(self): return { 'path': path, 'url': isReferencable(path) and path.replace('.', '/') or None} - return None def getSubdirectives(self): """Create a list of subdirectives.""" @@ -125,7 +123,7 @@ def getSubdirectives(self): for ns, name, schema, handler, info in self.context.subdirs: details = self._getInterfaceDetails(schema) path = getPythonPath(handler) - url = isReferencable(path) and path.replace('.', '/') or None + url = path.replace('.', '/') if isReferencable(path) else None dirs.append({ 'namespace': ns, 'name': name, diff --git a/src/zope/app/apidoc/zcmlmodule/browser.rst b/src/zope/app/apidoc/zcmlmodule/browser.rst index 64adb55e..0dc9c009 100644 --- a/src/zope/app/apidoc/zcmlmodule/browser.rst +++ b/src/zope/app/apidoc/zcmlmodule/browser.rst @@ -62,7 +62,7 @@ And we can get its menu title and link. >>> menu.getMenuTitle(node) 'page' >>> menu.getMenuLink(node) - 'http://127.0.0.1/++etc++site/++apidoc++/ZCML/http_co__sl__sl_namespaces.zope.org_sl_browser/page/index.html' + 'http://127.0.0.1/++apidoc++/ZCML/http_co__sl__sl_namespaces.zope.org_sl_browser/page/index.html' Note that the directive's namespace URL is encoded, so it can be used in a URL.