export name (PEP 842)

Summary

This import hook makes export a soft keyword, so that it automatically adds to __all__ the relevant names in the following cases:

export class ClassName ...

export def function_name ...

export name ... = ...

where these statements occur at the top level (i.e. not within a function or class definition). In some sense, it complements the from … export (PEP 843) import hook. In the next section, we demonstrate how we can combine these two import hooks.

Source code

This import hook was inspired by PEP 842 (withdrawn) which suggested the addition of export as a soft keyword to be used in the following three cases:

export name ... = ...
export def function_name(): ...
export class ClassName(): ...

Note that PEP 842 suggests a lot more than what we wrote above:

  • It suggest the creation of an __export__ list.

  • It suggest that using export other than in top-level statement should result in an ExportError.

  • It suggests the creation of a modified __dir__.

  • etc.

We will first start with an example that implements only the three cases we mentioned. Consider the following file:

# export_name_1.py

from math import pi

export PI = pi

export public = "public variable"

export def useful_fn():
    print("This is a very useful function")

def private():
    print("I want to be able to change my name.")

secret = "Ideas's code is a mess."

Let’s use our import hook to import this function using a standard Python interpreter.

> py
Python 3.11.9 ...
>>> from ideas.included.export_name import add_hook
>>> hook = add_hook()
>>> import export_name_1
>>> dir(export_name_1)
['PI', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'pi', 'private', 'public', 'secret', 'useful_fn']

Looking closely at the output, we can see that private, pi, and secret are not exposed, but it is not easy to see. However, they are still available.

>>> export_name_1.secret
"Ideas's code is a mess."

And, __all__ only shows the names we want, so we could quickly determine if it is safe to use a star-import.

>>> export_name_1.__all__
['PI', 'public', 'useful_fn']

A restricted dir

As we can see from the example above, when simply using dir it might be difficult to identify which names are “public”. Presumably for this reason, PEP 842 suggests that using export in a module should also result in creating a __dir__ function within this module so that Python’s dir function can be restricted to only show the desired names.

We have implemented a version of this idea, available as an option, demonstrated below.

> py
Python 3.11.9 ...
>>> from ideas.included.export_name import add_hook
>>> hook = add_hook(public_dir=True)  # optional argument
>>> import export_name_1
>>> dir(export_name_1)
['PI', 'public', 'useful_fn']

We can nonetheless still see all the available names that dir would have shown us before, and then some …

>>> list(vars(export_name_1))
['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', '__all__', '__dir__', 'pi', 'PI', 'public', 'useful_fn', 'private', 'secret']

Instead of creating a __dir__ function within the module, we prefer to use a simple function that we have written, which extracts the content of __all__ if it exists, otherwise it gives us what dir would give us normally, but not always in the same order.

> py
Python 3.11.9 ...
>>> from ideas.utils import pdir
>>> pdir()
['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'pdir']
>>> pdir().sort() == dir().sort()
True
>>> from ideas.included.export_name import add_hook
>>> hook = add_hook()
>>> import export_name_1
>>> pdir(export_name_1)
['PI', 'public', 'useful_fn']

As we can see, with a simple utility function, like pdir, we do not use to create a special __dir__ within a module.

Implementation

Let’s explore how this is implemented, like we did in the from … export (PEP 843) import hook.

> py
Python 3.11.9 (tags/v3.11.9:de54cf5, Apr  2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from ideas import transform
>>> from ideas.included.export_name import add_hook
>>> hook = add_hook()
>>> transform("export def test(): ...")

__all__ = globals().setdefault("__all__", [])
__all__ = list(__all__)
__all__.append('test')
def        test(): ...

We would have a similar result with class instead of def. The situation is slightly different for variables. First, a proper declaration.

>>> transform("export name = ...")

__all__ = globals().setdefault("__all__", [])
__all__ = list(__all__)
__all__.append('name')
name        = ...

However, if not assignment is done using an = sign, no transformation takes place.

>>> transform("export name ...")
export name ...

The same occurs if an export keyword is not used at the top level.

>>> transform("export name ...")
export name ...
>>> source = '''
... def test():
...     export name = 'Bob'
... '''
>>> transform(source)

def test():
    export name = 'Bob'

This would clearly cause a syntax error if it were to be executed.

Finally, let us give a single additional example with the public_dir option. However, we can’t simply use the transform function as it only takes a source as an argument and we need to tell our import hook other arguments to take into account. However, we can import a file, defined as follows:

# export_name_2.py
export name = 'Bob'

Here’s a sample session.

> py
Python 3.11.9 ...
>>> from ideas.included.export_name import add_hook
>>> from ideas import current_state
>>> current_state.show_changes = True
>>> hook = add_hook(public_dir=True)
>>> import export_name_2

#========== Original source from C:\Users\Andre\github\ideas\docs_examples\export_name_2.py ====
# flake8: noqa
# export_name_2.py
export name = 'Bob'
#=== End of Original source from C:\Users\Andre\github\ideas\docs_examples\export_name_2.py ====


#========== Transformed source ====
__all__ = globals().setdefault('__all__', [])
__dir__ = lambda: __all__
# flake8: noqa
# export_name_2.py

__all__ = globals().setdefault("__all__", [])
__all__ = list(__all__)
__all__.append('name')
name        = 'Bob'
#=== End of Transformed source ====

As we can see, at the top of the transformed source, a new __dir__ function has been introduced.

We will see how to combine our limited implementation of both PEP 842 and PEP 843 in the next section, export as a keyword.