Summary

This import hook enables someone to try out the syntax from ... export ... proposed in PEP 843. It is a first draft which is likely buggy. The import_hook.py code has grown organically as more examples were added and is in need of a serious cleanup.

Source code

PEP 843

PEP 843 suggests the addition of export as a soft keyword to be used in expressions of the basic form:

from x export y [as z]

with other slight variations described below. Assuming that __all__ = [...] is already defined, the statement

from x export y

would be equivalent to

from x import y
__all__.append(y)

Implementation

We implement this as a source transformation. PEP 843 suggests that:

from <module> import <name> as <alias>

should be equivalent to:

from <module> import <name> as <alias>
exported_names = globals().setdefault("__all__", [])
if not isinstance(exported_names, list):
    exported_names = list(exported_names)
    __all__ = exported_names
exported_names.append("<alias>")

We avoid introducing exported_names as an intermediary variable by doing something like the following instead:

from <module> import <name> as <alias>
__all__ = globals().setdefault("__all__", [])
__all__ = list(__all__)
__all__.extend(["<alias>"])

PEP 843 also states that “unlike import, export is restricted to module level: it’s a SyntaxError inside a def or class body.”

As such, we do not transform from ... export .. if it occurs within a class or function body.

Star version

For the star version:

from module export *

we believe that something like the following should do what is expected:

from [...]module import *
__all__ = globals().setdefault("__all__", [])
__all__ = list(__all__)
from {relative} import {module}
if hasattr({module}, "__all__"):
    __all__.extend(list({module}.__all__))
else:
    for _ in dir({module}):
        if not _.startswith("_"):
            __all__.append(_)
    del _

lazy keyword

While this transformation will insert “the right code” to replace:

lazy from ... export ...

by:

lazy from ... import ...
# some additional code here

the additional code inserted in the case of an export * will result in a non-lazy import. However, since this is just to provide a way to test the syntax proposed in PEP 843, and not actually be used in production, it should be no cause for concerns.

export as identifier

export can still be used as an identifier: it is only replaced by import on a top-level from ... export ... statement.

Example

Suppose that we have the following file structure:

hub/
   __init__.py
   mod_a.py
   mod_b.py
   mod_c.py
   subhub/
       __init__.py
       mod_d.py

with the following file contents:

# hub/__init__.py

if True:
    from .mod_a export Widget, Gadget, export
else:
    from .mod_a export NotWidget, NotGadget

from .mod_b export *

from .mod_c export (a,
    b,
c
)

# mod_d defines __all__ as a tuple
from .subhub.mod_d export *
# mod_a.py

class Widget:
    pass

class Gadget:
    pass

class NotWidget:
    pass

class NotGadget:
    pass

export = "safe name"
# mod_b.py

def cool():
    pass

def _cool():
    pass

def hot():
    pass

def _hot():
    pass
# mod_c.py

a = b = c = d = e = 1
# mod_d.py

spam = "spam"
ham = "ham"
not_spam = "not_spam"
not_ham = "not_ham"

# Note the use of a tuple instead of a list.
__all__ = ("spam", "ham")

Here is what an interactive session with the Ideas console looks like:

(venv-ideas3.11) C:\Users\Andre\github\ideas
> python -i -m ideas -a pep_843
Ideas Console version 0.2.0. [Python version: 3.11.9]
ideas> from hub import *
ideas> dir()
['Gadget', 'Widget', '__builtins__', 'a', 'b', 'c', 'cool', 'current_state', 'export', 'ham', 'hot', 'spam']
ideas> export
'safe name'
ideas>

And here’s a similar experiment done within the normal Python repl:

> py
Python 3.11.9 ...
>>> from ideas.examples.pep_843 import add_hook
>>> hook = add_hook()
>>> from hub import *
>>> dir()
['Gadget', 'Widget', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'add_hook', 'b', 'c', 'cool', 'export', 'ham', 'hook', 'hot', 'spam']
>>> export
'safe name'
>>>

Warning

Do not use continuation characters. The current transformation might not handle them correctly.