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:

from_export_hub/
   __init__.py
   mod_a.py
   mod_b.py
   sub_hub/
       __init__.py
       mod_c.py

with the following file contents:

# from_export_hub/__init__.py

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

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

# mod_c defines __all__ as a tuple
from from_export_hub.sub_hub.mod_c export *
# from_export_hub/mod_a.py

class Widget: pass

class Gadget: pass

class NotWidget: pass

class NotGadget: pass

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

a = b = c = d = e = f = g = 1
# from_export_hub/sub_hub/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.2.0. [Python version: 3.11.9]
ideas> dir()
['__builtins__', 'current_state']
ideas> from from_export_hub import *
ideas> dir()
['NewGadget', 'Widget', '__builtins__', 'a', 'b', 'c', 'current_state', 'export', 'ham', 'spam']
ideas> export
'A safe name'

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

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

> py
Python 3.11.9 ...
>>> from ideas.included.from_export import add_hook
>>> hook = add_hook()
>>> import from_export_hub
>>> from_export_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

To see the actual implementation, we can use the function transform which is available for that purpose.

First, we consider an “export” statement with names fully specified.

> py
Python 3.11.9 ...
>>> from ideas.included.from_export import add_hook
>>> hook = add_hook()
>>> from ideas import transform
>>> transform("    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:

>>> transform("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.

>>> transform("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.

>>> source = '''
... def test():
...     from math export pi
... '''
>>> transform(source)

def test():
    from math export pi

As we can see, it has not changed. If we put this code in a file named from_export_1.py and try to import it, here is the result.

… code-block:

>>> import from_export_1
An exception was raised while attempting to produce an AST.
File "C:/Users/Andre/github/ideas/docs_examples/from_export_1.py", line 5
    from math export pi
              ^^^^^^
SyntaxError: invalid syntax


You might want to use the command line flag --verbose or setting
session.current_state.verbose=True to get more details.
>>>