Summary
This import hook enables someone to try out the syntax
from ... export ... proposed in PEP 843.
It is a first draft which is likely buggy. The import_hook.py code has grown
organically as more examples were added and is in need of a serious cleanup.
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)
Implementation
We implement this as a source transformation. 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 avoid introducing exported_names as an intermediary
variable by doing something like the following instead:
from <module> import <name> as <alias>
__all__ = globals().setdefault("__all__", [])
__all__ = list(__all__)
__all__.extend(["<alias>"])
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.
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.
Example
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:
(venv-ideas3.11) C:\Users\Andre\github\ideas
> 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.examples.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'
>>>
Warning
Do not use continuation characters. The current transformation might not handle them correctly.