Summary
This import hook enables someone to try out the syntax
from ... export ... proposed in PEP 843.
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.