from … export … : PEP 843

Summary

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

In some sense, it complements the export name (PEP 842) import hook. In a later section, we demonstrate how we can combine these two import hooks.

Source code

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
   _internal/
       __init__.py
       mod_c.py

with the following file contents:

# hub/__init__.py

from hub.mod_a export Widget, Gadget as NewGadget, export

from hub.mod_b export (a,
    b,
    c,
)

# mod_c defines __all__ as a tuple
from hub._internal.mod_c export *
# hub/mod_a.py

class Widget: pass

class Gadget: pass

class NotWidget: pass

class NotGadget: pass

export = "A safe name"
# hub/mod_b.py

a = b = c = d = e = f = g = 1
# hub/_internal/mod_c.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:

> ideas -a from_export
Ideas Console version 0.3.4. [Python version: 3.11.9]
ideas> dir()
['__builtins__', 'ideas_state']
ideas> from hub import *
ideas> dir()
['NewGadget', 'Widget', '__builtins__', 'a', 'b', 'c', 'export', 'ham', 'ideas_state', 'spam']
ideas> export  # variable name unaffected
'A safe name'

As we can verify, only the names that were meant to be “exported” have been imported.

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

>>> from ideas.included.from_export import add_hook
>>> hook = add_hook()
>>> import hub
>>> hub.__all__
['Widget', 'NewGadget', 'export', 'a', 'b', 'c', 'spam', 'ham']

Proposed implementation

PEP 843 suggests that:

from <module> export <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 of this import hook

To see the actual implementation, we can use the recently added command line option --t of the ideas entry point to quickly see the result.

First, we consider an “export” statement with names fully specified, and arbitrarily indented to illustrate that the indentation is preserved.

> ideas -a from_export -t "       from a.b export A, B as C"
    from a.b import A, B as C
    __all__ = globals().setdefault("__all__", [])
    __all__ = list(__all__)
    __all__.extend(['A', 'C'])

Next, we look at the star version:

> ideas -a from_export -t "from module export *"
from module import *
__all__ = globals().setdefault("__all__", [])
__all__ = list(__all__)
from . import module
if hasattr(module, "__all__"):
    __all__.extend(list(module.__all__))
else:
    for _ in dir(module):
        if not _.startswith("_"):
            __all__.append(_)
    del _

Looking ahead we can also support the lazy keyword.

> ideas -a from_export -t "lazy from math export pi"
lazy from math import pi
__all__ = globals().setdefault("__all__", [])
__all__ = list(__all__)
__all__.extend(['pi'])

export as an identifier

As we have seen in the example above export can still be used as an identifier: it is only replaced by import on a top-level from ... export ... statement. Using such a statement anywhere else will result in a SyntaxError when the code is executed by Python. In the following example, we demonstrate this. Since we need to use a multiline example with indentation, we cannot do it with the -t option on the command line.

ideas> from ideas import transform
ideas> with open("from_export_1.py", "r") as f:
...     source = f.read()
...
ideas> print(source)
# from_export_1.py

def test():
    from math export pi

ideas> transform(source)
# from_export_1.py

def test():
    from math export pi

We can see that no source transformation took place. Now, let’s try to import this file:

ideas> import from_export_1
File "C:\Users\Andre\github\ideas\docs_examples\included\from_export\from_export_1.py", line 4
    from math export pi
              ^^^^^^
SyntaxError: invalid syntax