Skip to content Skip to sidebar Skip to footer

Export Decorator That Manages __all__

A proper Python module will list all its public symbols in a list called __all__. Managing that list can be tedious, since you'll have to list each symbol twice. Surely there are b

Solution 1:

In Is it a good practice to add names to __all__ using a decorator?, Ed L suggests the following, to be included in some utility library:

import sys

def export(fn):
    """Use a decorator to avoid retyping function/class names.

    * Based on an idea by Duncan Booth:
      http://groups.google.com/group/comp.lang.python/msg/11cbb03e09611b8a
    * Improved via a suggestion by Dave Angel:
      http://groups.google.com/group/comp.lang.python/msg/3d400fb22d8a42e1
    """
    mod = sys.modules[fn.__module__]
    if hasattr(mod, '__all__'):
        name = fn.__name__
        all_ = mod.__all__
        if name not in all_:
            all_.append(name)
    else:
        mod.__all__ = [fn.__name__]
    return fn

We've adapted the name to match the other examples. With this in a local utility library, you'd simply write

from .utility import export

and then start using @export. Just one line of idiomatic Python, you can't get much simpler than this. On the downside, the module does require access to the module by using the __module__ property and the sys.modules cache, both of which may be problematic in some of the more esoteric setups (like custom import machinery, or wrapping functions from another module to create functions in this module).

The python part of the atpublic package by Barry Warsaw does something similar to this. It offers some keyword-based syntax, too, but the decorator variant relies on the same patterns used above.

This great answer by Aaron Hall suggests something very similar, with two more lines of code as it doesn't use __dict__.setdefault. It might be preferable if manipulating the module __dict__ is problematic for some reason.


Solution 2:

You could simply declare the decorator at the module level like this:

__all__ = []

def export(obj):
    __all__.append(obj.__name__)
    return obj

This is perfect if you only use this in a single module. At 4 lines of code (plus probably some empty lines for typical formatting practices) it's not overly expensive to repeat this in different modules, but it does feel like code duplication in those cases.


Solution 3:

You could define the following in some utility library:

def exporter():
    all = []
    def decorator(obj):
        all.append(obj.__name__)
        return obj
    return decorator, all

export, __all__ = exporter()
export(exporter)

# possibly some other utilities, decorated with @export as well

Then inside your public library you'd do something like this:

from . import utility

export, __all__ = utility.exporter()

# start using @export

Using the library takes two lines of code here. It combines the definition of __all__ and the decorator. So people searching for one of them will find the other, thus helping readers to quickly understand your code. The above will also work in exotic environments, where the module may not be available from the sys.modules cache or where the __module__ property has been tampered with or some such.


Solution 4:

This is not a decorator approach, but provides the level of efficiency I think you're after.

https://pypi.org/project/auto-all/

You can use the two functions provided with the package to "start" and "end" capturing the module objects that you want included in the __all__ variable.

from auto_all import start_all, end_all

# Imports outside the start and end functions won't be externally availab;e.
from pathlib import Path

def a_private_function():
    print("This is a private function.")

# Start defining externally accessible objects
start_all(globals())

def a_public_function():
    print("This is a public function.")

# Stop defining externally accessible objects
end_all(globals())

The functions in the package are trivial (a few lines), so could be copied into your code if you want to avoid external dependencies.


Solution 5:

While other variants are technically correct to a certain extent, one might also be sure that:

  • if the target module already has __all__ declared, it is handled correctly;
  • target appears in __all__ only once:
# utils.py

import sys

from typing import Any


def export(target: Any) -> Any:
  """
  Mark a module-level object as exported.

  Simplifies tracking of objects available via wildcard imports.

  """
  mod = sys.modules[target.__module__]

  __all__ = getattr(mod, '__all__', None)

  if __all__ is None:
    __all__ = []
    setattr(mod, '__all__', __all__)

  elif not isinstance(__all__, list):
    __all__ = list(__all__)
    setattr(mod, '__all__', __all__)

  target_name = target.__name__
  if target_name not in __all__:
    __all__.append(target_name)

  return target

Post a Comment for "Export Decorator That Manages __all__"