Summary

This import hook enables someone to try out the syntax from ... export ... proposed in PEP 843.

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)

The main motivation of this PEP appears to be facilitating the maintenance of “large projects” which define their public interface within an __init__.py file, by importing various objects from the “private” subdirectories and exposing them to the public. This requires updating __all__ each time a new variable is to be made public.

This import hook implements a source transformation that aims to mimic the proposed changes described in PEP 843.

Example

The code in this section is from an example that we currently did with this import hook.

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:

> 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.included.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'
>>>

Implementation

Proposed implementation

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 implement something similar as a source transformation. However, we avoid introducing exported_names as an intermediary.

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. Such code will result in a SyntaxError.

Actual implementation

We will use a separate module, ignore.py (name chosen so that it is ignored locally by git) to use various variants of the from ... export ... statement to demonstrate what is being done.

Within a Python repl, we will use an option which

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

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.

Warning

Do not use continuation characters in your sample code. The current transformation might not handle them correctly.

Please report any bug you find.

Tip

You might want to combine this import hook with export keyword one described in the following section.