Welcome, guest | Sign In | My Account | Store | Cart

If you aren't very careful, modules can end up exporting more symbols than you intend. For example, everything that you import is added to your module's name-space. Then there's scaffolding and other stuff intended for internal use. The usual advice is to name everything with a leading underscore, but that gets complicated fast: "import sys as _sys", "from os import environ as _environ". The alternative is to use "__all__ " to define exactly what you want to export, but then you need to maintain it as you add things to your module. Or do you?

Python, 44 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
"""\
<<Your current doc-string goes here.>>

Note:  The following doctest verifies that we're exporting all of the
symbols that we're supposed to be.  First, we gather all of the
symbols imported using 'from X import *'. Next we look at our global
name-space, discarding those we definitely don't want and then only
keeping those that meet our criteria.  Finally, we print the differences
between what we found and what's actually being exported.

>>> from new import classobj
>>> exclusions = set()
>>> for name in ['Tkinter']:
...     exclusions.update(dir(__import__(name)))
>>> exports = set()
>>> namespace = globals().copy()
>>> for name in namespace:
...     if name in exclusions or name.startswith("_"):
...         pass
...     else:
...         if name == name.upper():
...             exports.add(name)
...         elif name.endswith('Icon'):
...             exports.add(name)
...         elif isinstance(namespace[name], classobj):
...             exports.add(name)
...         else:
...             pass

>>> print "Missing from __all__:", sorted(exports - set(__all__))
Missing from __all__: []

>>> print "Extraneous in __all__:", sorted(set(__all__) - exports)
Extraneous in __all__: []

"""

__all__ = []

<<Your module's code goes here.>>

if __name__ == '__main__':
    import doctest
    doctest.testmod()

The DRY principle says, "Don't repeat yourself". Python modules often define a global variable named "__all__" that holds the names of everything the author wants visible. This means you have to type each variable, function or class name a second time, and you also have to maintain the contents of __all__ manually. This fixes that.

First, you need a list of "polluting" modules. For example, Tkinter contains a lot of classes, and since no one likes to type "Tkinter" in front of every single object that they use, you see a lot of "from Tkinter import *" statements. In this case, Tkinter is polluting our name-space, so it goes into the "for name in ['PIL', 'Tkinter']:" statement.

Next, you need a way to decide what you want to export. Before doing that, however, I exclude all variables that occur in the polluting modules. Yes, this excludes anything I've created with the same name; I discuss this decision below. I also exclude everything whose name stats with an underscore; this is the same as what Python would do on its own, and gives me an escape hatch if I really want something excluded. Once I've passed on those names, I describe what should be exported: (1) names that are all uppercase, because those usually identify constant values, and it gives me an escape hatch if I really want something included; (2) names that end in "Icon", because I may want to use those icons in other places; and (3) objects that are old-style classes, because in Python 2.x, all classes defined by Tkinter are old-style, which means any derived class also have to be old-style. Luckily, Python's "new" module contains the superclass of all old-style classes, classobj, so I can code that test as easily as the others.

Finally, I find the differences between what is being exported, and what I think should be, and I print them out. The first time you run the doctest, you'll get output similar to this:

**********************************************************************
File "__main__", line 27, in __main__
Failed example:
    print "Missing from __all__:", sorted(exports - set(__all__))
Expected:
    Missing from __all__: []
Got:
    Missing from __all__: ['AutoScrollbar', ..., 'removeIcon']
**********************************************************************
1 items had failures:
   1 of   8 in __main__
***Test Failed*** 1 failures.

Simply copy the list in the "Got" message and paste it as the new value of __all__, and re-run; everything should be OK the second time through. Later, as you add new objects to your module, the doctest will notice that they don't appear in __all__ and remind you to add them.

P.S. Yes, figuring out those 'if' statements can be tricky, but it's something that you only need to do once. It helps if your modules only export one or two types of objects. For example, I used to have a memoization decorator in my module that I was also using elsewhere. But that function was unrelated to the other stuff in my module, so I moved it to a distinct module that only exported functions. My doctest was simpler, plus I could reuse my decorator without having to load a bunch of Tkinter stuff that I might not need. If something's really tricky, you can still use a leading underscore in the name; the doctest will exclude it.

P.P.S. I mentioned that names that are all uppercase are guaranteed to be exported... unless those names are also defined in one of your "polluting" modules. That's a feature. If I'm importing everything from Tkinter, and I'm also defining a name that duplicates one of those names, I'm risking breaking something. That's hard to catch, but hopefully I'll notice when that item isn't imported when I import my module elsewhere.

Created by Sam Denton on Wed, 19 Sep 2012 (MIT)
Python recipes (4591)
Sam Denton's recipes (5)

Required Modules

Other Information and Tasks