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