Skip to content

Commit

Permalink
Fix various forms of namespace packages not finding children.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jamadden committed May 19, 2017
1 parent fa5201c commit ac6818c
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 31 deletions.
30 changes: 19 additions & 11 deletions setup.py
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down
11 changes: 6 additions & 5 deletions src/zope/app/apidoc/browser/utilities.py
Expand Up @@ -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)
4 changes: 3 additions & 1 deletion src/zope/app/apidoc/codemodule/browser/class_.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
7 changes: 3 additions & 4 deletions src/zope/app/apidoc/codemodule/browser/function.py
Expand Up @@ -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):
Expand All @@ -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)
13 changes: 8 additions & 5 deletions src/zope/app/apidoc/codemodule/browser/menu.py
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion src/zope/app/apidoc/codemodule/browser/tests.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions src/zope/app/apidoc/codemodule/browser/zcml.py
Expand Up @@ -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
Expand Down Expand Up @@ -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' % (
Expand Down
25 changes: 24 additions & 1 deletion src/zope/app/apidoc/codemodule/module.py
Expand Up @@ -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.
Expand Down
39 changes: 39 additions & 0 deletions src/zope/app/apidoc/codemodule/tests.py
Expand Up @@ -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()

Expand Down
14 changes: 14 additions & 0 deletions src/zope/app/apidoc/codemodule/zcml.py
Expand Up @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/zope/app/apidoc/ftesting-base.zcml
Expand Up @@ -106,7 +106,7 @@
<include package="zope.app.preference" />
<include package="zope.app.renderer"/>
<include package="zope.app.authentication" />
<include package="zope.app.securitypolicy" />

<include package="zope.login" />
<include package="zope.security" />
<include package="zope.app.apidoc" file="meta.zcml" />
Expand Down

0 comments on commit ac6818c

Please sign in to comment.