Python Export Decorator In Python, we use __all__ to explicitly denote the public API of a module or package. That looks like this:

__all__ = ['Foo', 'bar', 'BAZ_CONSTANT']

BAZ_CONSTANT = None

class Foo:
    pass

def bar():
    pass


... except that modules can go on for thousands of lines, and then it is not immediately apparent, without revisiting the heading of the file, which objects are for external use, and which are not. 

We are also required to write the names of the objects twice for each of these objects. (At a minimum. Even more for unittests - and we write our tests first, don't we?)

What I want is a decorator that allows me to shift the declaration of the public API away from the heading and to the object and constant definitions, where we, in most cases, can only write the name to be exported once.

Here's a serviceable preliminary draft, targeted to Python 3.6+ - perhaps the simplest thing that could possibly work:

import sys

class ExportError(Exception):
    """Exception if redundant names attempted exporting"""

def export(obj, global_namespace: dict=None) -&gt; 'obj arg':
    """export adds names of functions and classes via decorator
    and module level variables via call (with globals() argument)
    to __all__.
    Usage:
    from exportlib import export

    export("BAZ_CONSTANT", globals()) # I don't like globals() call...
    BAZ_CONSTANT = None

    @export
    class Foo: 
        pass

    @export
    def bar(): 
        pass
    """
    name = obj if isinstance(obj, str) else obj.__name__
    if global_namespace is None:
         try:
             global_namespace = obj.__globals__
         except AttributeError as error:
             raise AttributeError(
               f'no globals, try export({name!r}, globals())')
    __all__ = global_namespace.setdefault('__all__', [])
    if name in __all__: # Do we care if redundant exports? Assuming so:
        raise ExportError(f'{name} already exported')
    __all__.append(name)
    return obj


Ok, could probably be a little simpler with fewer features. I do like the documenting features of the annotations. I couldn't identify a more appropriate Exception to subclass or use directly than Exception.

I don't like the globals() call required when adding a module level variable that doesn't know its own name or module, but the only alternative, that I can think of, is to do what super does and climb a stackframe up and get the globals there, or pass in __all__ instead (which would require creating it explicitly). Stackframe climbing would preclude export from being wrapped/extended.

But I think this makes a sufficiently sophisticated starting point.
Personally, I'd prefer to use @export() or @export(globals()), rather than manually perform the @ sugar.
I don't like using __globals__, even if it is writeable.
Honestly, I think the cleanest solution would be to abuse lists, and use a closure.
I do however agree that inheriting from Exception, is the best parent to inherit from.


And so I'd personally use something more like:

class ExportError(Exception):
    """Exception if redundant names attempted exporting"""

def build_export(__all__=None):
    if __all__ is None:
        __all__ = []
    def export(obj):
        name = obj if isinstance(obj, str) else obj.__name__
        if name in __all__:
            raise ExportError(f'{name} already exported')
        __all__.append(name)
        return obj
    return __all__, export


__all__, export = build_export()


@export
class Foo:
    pass


@export
def bar():
    pass