diff --git a/setup.py b/setup.py
index b1f979e1..61230b75 100644
--- a/setup.py
+++ b/setup.py
@@ -27,20 +27,28 @@ def read(*rnames):
return f.read()
tests_require = [
- 'zope.app.securitypolicy',
- 'zope.app.schema',
+ 'zope.app.authentication >= 4.0.0',
+ 'zope.app.folder >= 4.0.0',
+ 'zope.app.http >= 4.0.1',
+ 'zope.app.principalannotation >= 4.0.0',
+ 'zope.app.rotterdam >= 4.0.0',
+ 'zope.app.wsgi >= 4.1.0',
+ 'zope.applicationcontrol >= 4.0.0',
+
'zope.browserpage >= 4.1.0',
- 'zope.securitypolicy',
'zope.login',
+ 'zope.principalannotation',
+ 'zope.securitypolicy',
'zope.testing',
'zope.testrunner',
- 'zope.principalannotation',
- 'zope.app.http',
- 'zope.app.rotterdam >= 4.0.0',
- 'zope.app.principalannotation',
- 'zope.app.folder >= 4.0.0',
- 'zope.applicationcontrol >= 4.0.0',
- 'zope.app.wsgi',
+
+ # Things we don't use or configure, but which are
+ # picked up indirectly by other packages and
+ # need to be loaded to avoid errors running the
+ # full static export.
+ 'zope.app.component[test]',
+ 'zope.app.form[test]', # zc.sourcefactory
+ 'zope.app.schema[test]',
]
static_requires = tests_require
@@ -105,7 +113,7 @@ def read(*rnames):
'zope.app.exception >= 4.0.0',
'zope.app.onlinehelp >= 4.0.0',
'zope.app.preference >= 4.0.0',
- 'zope.app.publisher',
+ 'zope.app.publisher >= 4.0.0',
'zope.app.renderer >= 4.0.0',
'zope.app.tree >= 4.0.0',
'zope.cachedescriptors',
diff --git a/src/zope/app/apidoc/browser/utilities.py b/src/zope/app/apidoc/browser/utilities.py
index e7a3fc13..8f05aa5a 100644
--- a/src/zope/app/apidoc/browser/utilities.py
+++ b/src/zope/app/apidoc/browser/utilities.py
@@ -13,15 +13,16 @@
##############################################################################
"""Common Utilities for Browser View
-$Id$
"""
from zope.app.apidoc.apidoc import APIDocumentation
from zope.traversing.browser import absoluteURL
from zope.traversing.api import getParent
from zope.security.proxy import isinstance
-def findAPIDocumentationRootURL(context, request):
+def findAPIDocumentationRoot(context, request=None):
if isinstance(context, APIDocumentation):
- return absoluteURL(context, request)
- else:
- return findAPIDocumentationRootURL(getParent(context), request)
+ return context
+ return findAPIDocumentationRoot(getParent(context), request)
+
+def findAPIDocumentationRootURL(context, request):
+ return absoluteURL(findAPIDocumentationRoot(context, request), request)
diff --git a/src/zope/app/apidoc/codemodule/browser/class_.py b/src/zope/app/apidoc/codemodule/browser/class_.py
index 9f88a900..fc39cf76 100644
--- a/src/zope/app/apidoc/codemodule/browser/class_.py
+++ b/src/zope/app/apidoc/codemodule/browser/class_.py
@@ -28,6 +28,8 @@
from zope.app.apidoc.utilities import renderText, getFunctionSignature
from zope.app.apidoc.utilities import isReferencable
+from zope.app.apidoc.browser.utilities import findAPIDocumentationRoot
+
def getTypeLink(type, _NoneType=type(None)):
if type is _NoneType:
@@ -60,7 +62,7 @@ def getKnownSubclasses(self):
return entries
def _getCodeModule(self):
- apidoc = traverse(self.context, '/++apidoc++')
+ apidoc = findAPIDocumentationRoot(self.context)
return apidoc['Code']
def _listClasses(self, classes):
diff --git a/src/zope/app/apidoc/codemodule/browser/function.py b/src/zope/app/apidoc/codemodule/browser/function.py
index 34405842..30f56065 100644
--- a/src/zope/app/apidoc/codemodule/browser/function.py
+++ b/src/zope/app/apidoc/codemodule/browser/function.py
@@ -16,12 +16,11 @@
"""
__docformat__ = 'restructuredtext'
-from zope.traversing.api import getParent, traverse
+from zope.traversing.api import getParent
from zope.traversing.browser import absoluteURL
-
from zope.app.apidoc.utilities import renderText
-
+from zope.app.apidoc.browser.utilities import findAPIDocumentationRoot
from zope.app.apidoc.codemodule.browser.class_ import getTypeLink
class FunctionDetails(object):
@@ -48,6 +47,6 @@ def getAttributes(self):
def getBaseURL(self):
"""Return the URL for the API Documentation Tool."""
- apidoc = traverse(self.context, '/++apidoc++')
+ apidoc = findAPIDocumentationRoot(self.context)
m = apidoc['Code']
return absoluteURL(getParent(m), self.request)
diff --git a/src/zope/app/apidoc/codemodule/browser/menu.py b/src/zope/app/apidoc/codemodule/browser/menu.py
index 1ec93a23..8ef97b69 100644
--- a/src/zope/app/apidoc/codemodule/browser/menu.py
+++ b/src/zope/app/apidoc/codemodule/browser/menu.py
@@ -17,9 +17,12 @@
__docformat__ = 'restructuredtext'
import operator
+from zope.security.proxy import removeSecurityProxy
+
from zope.traversing.api import traverse
from zope.traversing.browser import absoluteURL
+from zope.app.apidoc.browser.utilities import findAPIDocumentationRoot
from zope.app.apidoc.classregistry import classRegistry
_pathgetter = operator.itemgetter("path")
@@ -57,7 +60,7 @@ def findClasses(self):
[{'path': 'zope.app.apidoc.codemodule.browser.menu.Men',
'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/codemodule/browser/menu/Menu/'},
{'path': 'zope.app.apidoc.ifacemodule.menu.Men',
- 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/ifacemodule/menu/Menu/'}]
+ 'url': 'http://127.0.0.1/++apidoc++/Code/zope/app/apidoc/ifacemodule/menu/Menu/'}...]
>>> menu.request = TestRequest(form={'path': 'illegal name'})
>>> info = menu.findClasses()
@@ -68,8 +71,8 @@ def findClasses(self):
path = self.request.get('path', None)
if path is None:
return []
- classModule = traverse(self.context, '/++apidoc++')['Code']
- classModule.setup()
+ classModule = findAPIDocumentationRoot(self.context)['Code']
+ removeSecurityProxy(classModule).setup()
found = [p for p in classRegistry if path in p]
results = []
for p in found:
@@ -108,8 +111,8 @@ def findAllClasses(self):
>>> len(info)
1
"""
- classModule = traverse(self.context, '/++apidoc++')['Code']
- classModule.setup() # run setup if not yet done
+ classModule = findAPIDocumentationRoot(self.context)['Code']
+ removeSecurityProxy(classModule).setup() # run setup if not yet done
results = []
counter = 0
diff --git a/src/zope/app/apidoc/codemodule/browser/tests.py b/src/zope/app/apidoc/codemodule/browser/tests.py
index da2363f6..2699a532 100644
--- a/src/zope/app/apidoc/codemodule/browser/tests.py
+++ b/src/zope/app/apidoc/codemodule/browser/tests.py
@@ -16,6 +16,8 @@
"""
import unittest
+from zope.traversing.api import traverse
+
from zope.app.apidoc.testing import APIDocLayer
from zope.app.apidoc.tests import BrowserTestCase
from zope.app.apidoc.tests import LayerDocFileSuite
@@ -121,7 +123,7 @@ def test_listClasses_C(self):
details = ClassDetails()
details.request = TestRequest()
- details.context = self.layer.getRootFolder()
+ details.context = traverse(self.layer.getRootFolder(), '/++apidoc++')
info = details._listClasses([items_class])
self.assertIsNone(info[0]['url'], None)
diff --git a/src/zope/app/apidoc/codemodule/browser/zcml.py b/src/zope/app/apidoc/codemodule/browser/zcml.py
index 7c3393c0..4ef4f331 100644
--- a/src/zope/app/apidoc/codemodule/browser/zcml.py
+++ b/src/zope/app/apidoc/codemodule/browser/zcml.py
@@ -15,16 +15,18 @@
"""
__docformat__ = "reStructuredText"
-from zope.component import getUtility
+
from zope.configuration.fields import GlobalObject, GlobalInterface, Tokens
from zope.interface.interfaces import IInterface
from zope.schema import getFieldNamesInOrder
from zope.security.proxy import isinstance, removeSecurityProxy
from zope.traversing.api import getParent
+from zope.traversing.api import traverse
from zope.traversing.browser import absoluteURL
from zope.app.apidoc.interfaces import IDocumentationModule
from zope.app.apidoc.utilities import getPythonPath, isReferencable
+from zope.app.apidoc.browser.utilities import findAPIDocumentationRoot
from zope.app.apidoc.browser.utilities import findAPIDocumentationRootURL
from zope.app.apidoc.zcmlmodule import quoteNS
@@ -76,7 +78,7 @@ def url(self):
# Sometimes ns is `None`, especially in the slug files, where no
# namespaces are used.
ns = quoteNS(ns or 'ALL')
- zcml = getUtility(IDocumentationModule, 'ZCML')
+ zcml = findAPIDocumentationRoot(self.context, self.request)['ZCML']
if name not in zcml[ns]:
ns = 'ALL'
link = '%s/ZCML/%s/%s/index.html' % (
diff --git a/src/zope/app/apidoc/codemodule/module.py b/src/zope/app/apidoc/codemodule/module.py
index 1a12bda4..bbb2030d 100644
--- a/src/zope/app/apidoc/codemodule/module.py
+++ b/src/zope/app/apidoc/codemodule/module.py
@@ -72,13 +72,36 @@ def __init__(self, parent, name, module, setup=True):
def __setup_package(self):
# Detect packages
module_file = getattr(self._module, '__file__', '')
+ module_path = getattr(self._module, '__path__', None)
if module_file.endswith(('__init__.py', '__init__.pyc', '__init__.pyo')):
self._package = True
+ elif hasattr(self._module, '__package__'):
+ # Detect namespace packages, especially (but not limited
+ # to) Python 3 with implicit namespace packages:
+
+ # "When the module is a package, its
+ # __package__ value should be set to its __name__. When
+ # the module is not a package, __package__ should be set
+ # to the empty string for top-level modules, or for
+ # submodules, to the parent package's "
+
+ # Note that everything has __package__ on Python 3, but not
+ # necessarily on Python 2.
+ pkg_name = self._module.__package__
+ self._package = pkg_name and self._module.__name__ == pkg_name
+ else:
+ # Python 2. Lets do some introspection. Namespace packages
+ # often have an empty file. Note that path isn't necessarily
+ # indexable.
+ if (module_file == ''
+ and module_path
+ and os.path.isdir(list(module_path)[0])):
+ self._package = True
if not self._package:
return
- for mod_dir in self._module.__path__:
+ for mod_dir in module_path:
# TODO: If we are dealing with eggs, we will not have a
# directory right away. For now we just ignore zipped eggs;
# later we want to unzip it.
diff --git a/src/zope/app/apidoc/codemodule/tests.py b/src/zope/app/apidoc/codemodule/tests.py
index 92e3e7e7..18d05e17 100644
--- a/src/zope/app/apidoc/codemodule/tests.py
+++ b/src/zope/app/apidoc/codemodule/tests.py
@@ -96,6 +96,45 @@ def test_hookable(self):
mod = Module(None, 'hooks', zope.component._api)
self.assertIsInstance(mod._children['getSiteManager'], Function)
+ def test_zope_loaded_correctly(self):
+ # Zope is guaranteed to be a namespace package, as is zope.app.
+ import zope
+ import zope.app
+ import zope.annotation
+ import zope.app.apidoc
+ mod = Module(None, 'zope', zope)
+ self.assertEqual(mod['annotation']._module, zope.annotation)
+ self.assertEqual(mod['app']._module, zope.app)
+ self.assertEqual(mod['app']['apidoc']._module, zope.app.apidoc)
+
+class TestZCML(unittest.TestCase):
+
+ def setUp(self):
+ from zope.app.apidoc.tests import _setUp_AppSetup
+ _setUp_AppSetup()
+
+ def tearDown(self):
+ from zope.app.apidoc.tests import _tearDown_AppSetup
+ _tearDown_AppSetup()
+
+ def test_installed(self):
+ from zope.app.apidoc.codemodule.zcml import MyConfigHandler
+ handler = MyConfigHandler(None)
+ self.assertTrue(handler.evaluateCondition('installed zope'))
+ self.assertFalse(handler.evaluateCondition('installed not-a-package'))
+
+ def test_copy_with_root(self):
+ from zope.app.apidoc.codemodule.zcml import ZCMLFile
+ fname = os.path.join(here, '..', 'ftesting-base.zcml')
+ zcml = ZCMLFile(fname, zope.app.apidoc,
+ None, None)
+
+ zcml.rootElement
+ self.assertEqual(zcml, zcml.rootElement.__parent__)
+
+ clone = zcml.withParentAndName(self, 'name')
+ self.assertEqual(clone.rootElement.__parent__, clone)
+
def test_suite():
checker = standard_checker()
diff --git a/src/zope/app/apidoc/codemodule/zcml.py b/src/zope/app/apidoc/codemodule/zcml.py
index c061c09a..390be91c 100644
--- a/src/zope/app/apidoc/codemodule/zcml.py
+++ b/src/zope/app/apidoc/codemodule/zcml.py
@@ -23,6 +23,7 @@
from zope.configuration import xmlconfig, config
from zope.interface import implementer, directlyProvides
from zope.location.interfaces import ILocation
+from zope.location.location import LocationProxy
import zope.app.appsetup.appsetup
@@ -44,6 +45,12 @@ def startPrefixMapping(self, prefix, uri):
def evaluateCondition(self, expression):
# We always want to process/show all ZCML directives.
+ # The exception needs to be `installed` that evaluates to False;
+ # if we can't load the package, we can't process the file
+ arguments = expression.split(None)
+ verb = arguments.pop(0)
+ if verb in ('installed', 'not-installed'):
+ return super(MyConfigHandler, self).evaluateCondition(expression)
return True
def startElementNS(self, name, qname, attrs):
@@ -110,6 +117,13 @@ def __init__(self, filename, package, parent, name):
self.__parent__ = parent
self.__name__ = name
+ def withParentAndName(self, parent, name):
+ located = type(self)(self.filename, self.package, parent, name)
+ # We don't copy the root element; let it parse again if needed, instead
+ # of trying to recurse through all the children and copy them.
+ return located
+
+
@Lazy
def rootElement(self):
# Get the context that was originally generated during startup and
diff --git a/src/zope/app/apidoc/ftesting-base.zcml b/src/zope/app/apidoc/ftesting-base.zcml
index 43a8a4ef..a9bc4df6 100644
--- a/src/zope/app/apidoc/ftesting-base.zcml
+++ b/src/zope/app/apidoc/ftesting-base.zcml
@@ -106,7 +106,7 @@
-
+