From ac6818cac0e5c4fdfbfce62c3c50953a11cad68a Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Fri, 19 May 2017 15:07:03 -0500 Subject: [PATCH] Fix various forms of namespace packages not finding children. The 'zope' node in the tree was missing in pip installs. Delegate installed/not-installed to the actual ZCML implementation so that we don't wind up trying to run python 2/3 specific directives on incorrect platforms, since that's more common now. Also clean up dependencies and remove dep on zope.app.securitypolicy which hasn't actually been ported to Python 3. --- setup.py | 30 ++++++++------ src/zope/app/apidoc/browser/utilities.py | 11 +++--- .../app/apidoc/codemodule/browser/class_.py | 4 +- .../app/apidoc/codemodule/browser/function.py | 7 ++-- .../app/apidoc/codemodule/browser/menu.py | 13 ++++--- .../app/apidoc/codemodule/browser/tests.py | 4 +- .../app/apidoc/codemodule/browser/zcml.py | 6 ++- src/zope/app/apidoc/codemodule/module.py | 25 +++++++++++- src/zope/app/apidoc/codemodule/tests.py | 39 +++++++++++++++++++ src/zope/app/apidoc/codemodule/zcml.py | 14 +++++++ src/zope/app/apidoc/ftesting-base.zcml | 2 +- 11 files changed, 124 insertions(+), 31 deletions(-) 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 @@ - +