Skip to content

Commit

Permalink
Merge pull request #21 from zopefoundation/issue20
Browse files Browse the repository at this point in the history
Make handling of root Code modules more consistent.
  • Loading branch information
jamadden committed Aug 9, 2018
2 parents fe2d7f7 + cd7372c commit 221cd44
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 17 deletions.
9 changes: 9 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@

- Add support for Python 3.7.

- The root ``Code`` documentation node no longer allows incidental
traversal and documentation of unregistered root modules such as
``re`` and ``logging`` (``builtins`` is special cased). These were
not listed in the tables of contents or menus, and mostly served to
slow down static exports. To document a root module, explicitly
include it in ZCML with ``<apidoc:rootModule module="MODULE" />``.
See `issue #20
<https://github.com/zopefoundation/zope.app.apidoc/issues/20>`_.

- Fix ``codemodule.Module`` for modules that have a ``__file__`` of
``None``. This can be the case with namespace packages, especially
under Python 3.7. See `issue #17 <https://github.com/zopefoundation/zope.app.apidoc/issues/17>`_.
Expand Down
2 changes: 1 addition & 1 deletion src/zope/app/apidoc/codemodule/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ obviously a bit different:
True

>>> sorted(cm.keys())
[u'BTrees', u'ZConfig', u'ZODB', u'persistent', u'transaction', ...]
[u'BTrees', u'ZConfig', u'ZODB', u'builtins', u'persistent', u'transaction', ...]


Module
Expand Down
3 changes: 3 additions & 0 deletions src/zope/app/apidoc/codemodule/browser/class_.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ def _listClasses(self, classes):
except TraversalError:
# If one of the classes is implemented in C, we will not
# be able to find it.
# Likewise, if we are attempting to get a root module that
# was not a registered root, the CodeModule will not be able to
# find it either.
pass
info.append({'path': path or None, 'url': url})
return info
Expand Down
12 changes: 9 additions & 3 deletions src/zope/app/apidoc/codemodule/codemodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ def setup(self):
if module is not None:
self._children[name] = Module(self, name, module)

# And the builtins are always available, since that's the
# most common root module linked to from docs.
builtin_module = safe_import('__builtin__') or safe_import('builtins')
assert builtin_module is not None
builtin_module = Module(self, builtin_module.__name__, builtin_module)
# Register with both names for consistency in the tests between Py2 and Py3
self._children['builtins'] = self._children['__builtin__'] = builtin_module


def withParentAndName(self, parent, name):
located = type(self)()
located.__parent__ = parent
Expand All @@ -99,9 +108,6 @@ def isPackage(self):

def get(self, key, default=None):
self.setup()
# TODO: Do we really like that this allows importing things from
# outside our defined namespace? This can lead to a static
# export with unreachable objects (not in the menu)
return super(CodeModule, self).get(key, default)

def items(self):
Expand Down
35 changes: 24 additions & 11 deletions src/zope/app/apidoc/codemodule/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,28 @@ def get(self, key, default=None):
if obj is not default:
return obj

# We are actually able to find much more than we promise

if self.getPath():
# Look for a nested module we didn't previously discover.

# Note that when path is empty, that means we are the global
# module (CodeModule) and if we get here, we're asking to find
# a module outside of the registered root modules. We don't
# look for those things.

# A common case for this to come up is 'menus' for the 'zope.app'
# module. The 'menus' module is dynamically generated through ZCML.

path = self.getPath() + '.' + key
else:
path = key
obj = safe_import(path)

if obj is not None:
child = Module(self, key, obj)
# But note that we don't hold on to it. This is a transient
# object, almost certainly not actually in our namespace.
# TODO: Why do we even allow this? It leads to much larger static exports
# and things that aren't even reachable from the menus.
return child
obj = safe_import(path)

if obj is not None:
self._children[key] = child = Module(self, key, obj)
# Caching this in _children may be pointless, we were
# most likely a copy using withParentAndName in the
# first place.
return child

# Maybe it is a simple attribute of the module
obj = getattr(self._module, key, default)
Expand All @@ -268,6 +276,11 @@ def items(self):
for name, value in self._children.items()
if not name.startswith('_')]

def __repr__(self):
return '<Module %r name %r parent %r at 0x%x>' % (
self._module, self.__name__, self.__parent__, id(self)
)

class _LazyModule(Module):

copy_from = None
Expand Down
17 changes: 17 additions & 0 deletions src/zope/app/apidoc/codemodule/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,23 @@ class Mod(object):
self.assertEqual(mod.__doc__, inst.getDocString())


class TestCodeModule(unittest.TestCase):

def test_find_builtins(self):
# CodeModule can always find builtins
# root modules.
import zope.app.apidoc.codemodule.codemodule
cm = zope.app.apidoc.codemodule.codemodule.CodeModule()
self.assertIsNotNone(cm.get('builtins'))

def test_not_find_logging(self):
# CodeModule other unregistered root modules,
# like logging, are not implicitly found.
import zope.app.apidoc.codemodule.codemodule
cm = zope.app.apidoc.codemodule.codemodule.CodeModule()
self.assertIsNone(cm.get('logging'))


class TestZCML(unittest.TestCase):

def setUp(self):
Expand Down
6 changes: 5 additions & 1 deletion src/zope/app/apidoc/ifacemodule/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,11 @@ def __call__(self):
mod_names = iface.__module__.split('.')
obj = codeModule
for name in mod_names:
obj = traverse(obj, name)
try:
obj = traverse(obj, name)
except KeyError: # pragma: no cover
# An unknown (root) module, such as logging
continue
crumbs.append({
'name': name,
'url': absoluteURL(obj, self.request)
Expand Down
13 changes: 12 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
[tox]
envlist = py27,py34,py35,py36,py37,pypy
envlist = py27,py34,py35,py36,py37,pypy,coverage

[testenv]
commands =
zope-testrunner --test-path=src []
deps =
.[test]

[testenv:coverage]
usedevelop = true
basepython =
python3.6
commands =
coverage run -m zope.testrunner --test-path=src []
coverage report --fail-under=99
deps =
{[testenv]deps}
coverage

0 comments on commit 221cd44

Please sign in to comment.