Skip to content

Commit

Permalink
Cope with unicode __all__ in Python 2 packages
Browse files Browse the repository at this point in the history
When porting a package from Python 2 to 3, one natural path involves
adding `from __future__ import unicode_literals` everywhere to prepare
for the `bytes`/`unicode` change.  This can cause `__all__` to contain
`unicode` elements, which mostly works but breaks star imports as
follows (depending on the exact Python version - see
https://bugs.python.org/issue21720):

    TypeError: Item in ``from list'' not a string

    TypeError: Item in ``from list'' must be str, not unicode

Star imports can usually be avoided, but it's hard to avoid this
behaviour of zope.configuration if you're using ZCML, so it seems worth
adjusting `ConfigurationContext.resolve` slightly to avoid the problem.
The `sys.modules` logic is borrowed from 2.7's
`importlib.import_module`.  (Using `importlib` directly here is tricky
because of the care we take with tracebacks, but for the time being we
can still get by with `__import__`.)
  • Loading branch information
cjwatson committed Feb 2, 2018
1 parent 24d5808 commit 10c1b82
Show file tree
Hide file tree
Showing 4 changed files with 29 additions and 6 deletions.
4 changes: 2 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Changes
4.1.1 (unreleased)
------------------

- Nothing changed yet.

- Fix resolving names from a Python 2 package whose `__init__.py` has
unicode elements in `__all__`.

4.1.0 (2017-04-26)
------------------
Expand Down
9 changes: 5 additions & 4 deletions src/zope/configuration/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@
metans = 'http://namespaces.zope.org/meta'
testns = 'http://namespaces.zope.org/test'

_import_chickens = {}, {}, ("*",) # dead chickens needed by __import__


class ConfigurationContext(object):
"""Mix-in that implements IConfigurationContext
Expand Down Expand Up @@ -148,7 +146,8 @@ def resolve(self, dottedname):
oname = ''

try:
mod = __import__(mname, *_import_chickens)
__import__(mname)
mod = sys.modules[mname]
except ImportError as v:
if sys.exc_info()[2].tb_next is not None:
# ImportError was caused deeper
Expand All @@ -167,7 +166,9 @@ def resolve(self, dottedname):
except AttributeError:
# No such name, maybe it's a module that we still need to import
try:
return __import__(mname+'.'+oname, *_import_chickens)
moname = mname + '.' + oname
__import__(moname)
return sys.modules[moname]
except ImportError:
if sys.exc_info()[2].tb_next is not None:
# ImportError was caused deeper
Expand Down
17 changes: 17 additions & 0 deletions src/zope/configuration/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@ def test_resolve_bad_sub_import(self):
if name in sys.modules:
del sys.modules[name]

def test_resolve_unicode_all(self):
# If a package's __init__.py is in the process of being ported from
# Python 2 to Python 3 using unicode_literals, then it can end up
# with unicode items in __all__, which breaks star imports from that
# package; but we tolerate this.
import sys
c = self._makeOne()
self.assertEqual(
c.resolve('zope.configuration.tests.unicode_all').foo, 'sentinel')
self.assertEqual(
c.resolve('zope.configuration.tests.unicode_all.foo'), 'sentinel')
# Cleanup:
for name in ('zope.configuration.tests.unicode_all',
'zope.configuration.tests.unicode_all.__future__'):
if name in sys.modules:
del sys.modules[name]

def test_path_w_absolute_filename(self):
import os
c = self._makeOne()
Expand Down
5 changes: 5 additions & 0 deletions src/zope/configuration/tests/unicode_all/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from __future__ import unicode_literals

__all__ = ['foo']

foo = 'sentinel'

0 comments on commit 10c1b82

Please sign in to comment.