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) -> '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.