1

What should I do, in a world where all text literals are Unicode by default, to make __import__ work in both Python 2 and 3?

I'm slowly learning about making Python code that will run under both Python 2 (version 2.6 or above) and Python 3 (version 3.2 or above).

This entails, I believe, the admonition to ensure text literals are Unicode by default:

from __future__ import unicode_literals

and to specify bytes literals explicitly with b'wibble' if needed.

The __import__ built-in function, though, is tripping up.

A contrived, trivial project layout:

$ mkdir fooproject/
$ cd fooproject/

$ mkdir foo/
$ printf "" > foo/__init__.py
$ mkdir foo/bar/
$ printf "" > foo/bar/__init__.py

Here's a simple fooproject/setup.py for that project:

from __future__ import unicode_literals

main_module_name = 'foo'
main_module = __import__(main_module_name, fromlist=['bar'])

assert main_module.bar

That fails under Python 2, but runs fine under Python 3:

$ python2 ./setup.py
Traceback (most recent call last):
  File "./setup.py", line 4, in <module>
    main_module = __import__(main_module_name, fromlist=['bar'])
TypeError: Item in ``from list'' not a string

$ python3 ./setup.py

We've deliberately made unadorned strings Unicode by default. By “not a string”, I presume Python 2 means “not a ‘bytes’ object”.

Okay, so we'll explicitly set that to a bytes literal:

from __future__ import unicode_literals

main_module_name = 'foo'
main_module = __import__(main_module_name, fromlist=[b'bar'])

assert main_module.bar

Now Python 2 is satisfied, but Python 3 complains:

$ python2 ./setup.py

$ python3 ./setup.py
Traceback (most recent call last):
  File "./setup.py", line 4, in <module>
    main_module = __import__(main_module_name, fromlist=[b'bar'])
  File "<frozen importlib._bootstrap>", line 2281, in
    _handle_fromlist
TypeError: hasattr(): attribute name must be string

So I've deliberately set unadorned strings to be Unicode by default, just as I'm supposed to; but that's apparently breaking the expectations of __import__ between Python 2 and Python 3.

How can I get that __import__ call, complete with its fromlist parameter, working correctly under both Python 2 and Python 3, keeping the unicode_literals setting?

2
  • You could write a function that you pass the string to, and it checks the version and returns either the unicode or bytestring version. Commented Dec 15, 2014 at 5:33
  • @BrenBarn, that's exactly how str() behaves! Commented Dec 15, 2014 at 6:25

2 Answers 2

2

recall str works differently for python2 and python3 (see @BrenBarn's comment), so:

main_module = __import__(main_module_name, fromlist=[str('bar')])

or more generally

main_module = __import__(main_module_name, fromlist=list(map(str, ['bar'])))
Sign up to request clarification or add additional context in comments.

1 Comment

A kludge for something which shouldn't be needed; but much neater than what I had before. Thanks.
1

The best I can come up with so far is a wrapper function to convert the type depending on Python version:

from __future__ import unicode_literals

import sys

fromlist_expects_type = str
if sys.version_info < (3, 0):
    fromlist_expects_type = bytes

def import_module(
        name, globals=None, locals=None, fromlist=(), level=0):
    """ Import specified module, together with options used by __import__.

        :param module_name: Text string of the module name to import.
        :param fromlist: List of names of attributes in the module to
            also import.
        :return: The module object.

        The built-in ``__import__`` function accepts a ``fromlist``
        parameter, but expects different string types between Python 2
        and Python 3. Python 2 only allows text string items; Python 3
        only allows byte string items.

        This function's ``fromlist`` parameter must have items of text
        string (Unicode) type only; the items are converted depending
        on the running Python version.

        """
    module_fromlist = ()
    if fromlist:
        module_fromlist = [
                fromlist_expects_type(attr_name) for attr_name in fromlist]
    module = __import__(
            name=name,
            globals=globals,
            locals=locals,
            fromlist=module_fromlist,
            level=level)

    return module

main_module_name = 'foo'
main_module = import_module(main_module_name, fromlist=['bar'])

That's pretty unwieldy, and I've probably made several mistakes. Surely this is a bug in Python that I need to do this?

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.