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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
|
==========================
Using @public and @private
==========================
This library provies two very simple decorators that document the *publicness*
of the names in your module. They keep your module's ``__all__`` in sync so
you don't have to.
Background
==========
``__all__`` is great. It has both functional and documentation purposes.
The functional purpose is that it `directly controls`_ which module names are
imported by the ``from <module> import *`` statement. In the absence of an
``__all__``, when this statement is executed, every name in ``<module>`` that
does not start with an underscore will be imported. This often leads to
importing too many names into the module. That's a good enough reason not to
use ``from <module> import *`` with modules that don't have an ``__all__``.
In the presence of an ``__all__``, only the names specified in this list are
imported by the ``from <module> import *`` statement. This in essence gives
the ``<module>`` author a way to explicitly state which names are for public
consumption.
And that's the second purpose of ``__all__``; it serves as module
documentation, explicitly naming the public objects it wants to export. You
can print a module's ``__all__`` and get an explicit declaration of its public
API.
The problem with __all__
========================
``__all__`` has two problems.
First, it separates the declaration of a name's public export semantics from
the implementation of that name. Usually the ``__all__`` is put at the top of
the module, although this isn't required, and in some cases it's `actively
prohibited`_. So when you're looking at the definition of a function or class
in a module, you have to search for the ``__all__`` definition to know whether
the function or class is intended for public consumption.
This leads to the second problem, which is that it's too easy for the
``__all__`` to get `out of sync`_ with the module's contents. Often a
function or class is renamed, removed, or added without the ``__all__`` being
updated. Then it's difficult to know what the module author's intent was, and
it can lead to an exception when a string appearing in ``__all__`` doesn't
match an existing name in the module. Some tools like Sphinx_ will complain
when names appear in ``__all__`` don't appear in the module. All of this
points to the root problem; it should be easy to keep ``__all__`` in sync!
The solution
============
This package provides a way to declare a name's *publicness* right at the
point of its declaration, and to infer the name to export from that
definition. In this way, a module's author never explicitly sets the
``__all__`` so there's no way for it to get out of sync.
This package, and Python `issue 26632`_, propose just such a solution, in the
form of a ``public`` builtin that can be used as either a decorator, or a
callable.
>>> from public import public
You'll usually use this as a decorator, for example::
>>> @public
... def foo():
... pass
or::
>>> @public
... class Bar:
... pass
The ``__all__`` after both of those code snippets has both names in it::
>>> print(__all__)
['foo', 'Bar']
Note that you do not need to initialize ``__all__`` in the module, since
``public`` will do it for you. Of course, if your module *already* has an
``__all__``, it will add any new names to the existing list.
Function call form
==================
The requirements to use the ``@public`` decorator are simple: the decorated
thing must have a ``__name__`` attribute. Since you'll overwhelmingly use it
to decorate functions and classes, this will always be the case. If the
object has a ``__module__`` attribute, that string is used to look up the
module object in ``sys.modules``, otherwise the module is extracted from the
globals where the decorator is called.
There's one other common use case that isn't covered by the ``@public``
decorator. Sometimes you want to declare simple constants or instances as
publicly available. You can't use the ``@public`` decorator for two reasons:
constants don't have a ``__name__`` and Python's syntax doesn't allow you to
decorate such constructs.
To solve this use case, ``public`` is also a callable function accepting
keyword arguments. An example makes this obvious. We'll start by resetting
the ``__all__``.
>>> reset()
>>> public(SEVEN=7)
7
>>> public(a_bar=Bar())
<...Bar object ...>
The module's ``__all__`` now contains both of the keys::
>>> print(__all__)
['SEVEN', 'a_bar']
and as should be obvious, the module contains name bindings for these
constants::
>>> print(SEVEN)
7
>>> print(a_bar)
<....Bar object at ...>
Multiple keyword arguments are allowed::
>>> public(ONE=1, TWO=2)
(1, 2)
>>> print(__all__)
['SEVEN', 'a_bar', 'ONE', 'TWO']
>>> print(ONE)
1
>>> print(TWO)
2
You'll notice that the functional form of ``public()`` returns the values in
its keyword arguments in order. This is to help with a use case where some
linters complain bcause they can't see that ``public()`` binds the names in
the global namespace. In the above example they might report erroneously that
``ONE`` and ``TWO`` aren't defined. To work around this, when ``public()`` is
used in its functional form, it will return the values in the order they are
seen [#]_ and you can simply assign them to explicit local variable names.
>>> a, b, c = public(a=3, b=2, c=1)
>>> print(__all__)
['SEVEN', 'a_bar', 'ONE', 'TWO', 'a', 'b', 'c']
>>> print(a, b, c)
3 2 1
It also works if you bind only a single value.
>>> d = public(d=9)
>>> print(__all__)
['SEVEN', 'a_bar', 'ONE', 'TWO', 'a', 'b', 'c', 'd']
>>> print(d)
9
@private
========
You might also want to be explicit about your private, i.e. non-public names.
This library also provides an ``@private`` decorator for this purpose. While
it mostly serves for documentation purposes, this decorator also ensures that
the decorated object's name does *not* appear in the ``__all__``. As above,
we'll start by resetting ``__all__``::
>>> reset()
>>> from public import private
>>> @private
... def foo():
... pass
>>> print(__all__)
[]
You can see here that ``foo`` has been removed from the ``__all__``. It's
okay if the name doesn't appear in ``__all__`` at all::
>>> @private
... class Baz:
... pass
>>> print(__all__)
[]
In this case, ``Baz`` never appears in ``__all__``. Like with ``@public``,
the ``@private`` decorator will initialize ``__all__`` if needed, but if it
exists in the module, it must be a list. There is no functional API for
``@private``.
Caveats
=======
There are some important usage restrictions you should be aware of:
* Only use ``@public`` and ``@private`` on top-level object. Specifically,
don't try to use either decorator on a class method name. While the
declaration won't fail, you will get an exception when you attempt to ``from
<module> import *`` because the name pulled from ``__all__`` won't be in the
module's globals.
* If you explicitly set ``__all__`` in your module, be sure to set it to a
list. Some style guides require ``__all__`` to be a tuple, but since that's
immutable, as soon as ``@public`` tries to append to it, you will get an
exception. Best practice is to not set ``__all__`` explicitly; let
``@public`` and ``@private`` do it!
* If you still want ``__all__`` to be immutable, put the following at the
bottom of your module::
__all__ = tuple(__all__)
Alternatives
============
This isn't a unique approach to ``@public``. Other_ implementations_ do
exist. There are some subtle differences between this package and those
others. This package:
* uses keyword arguments to map names which don't have an ``__name__``
attribute;
* can be used to bind names and values into a module's globals;
* can optionally put ``public`` in builtins.
.. rubric:: Footnotes
.. [#] This is ordering is guaranteed by `PEP 468 <https://peps.python.org/pep-0468/>`_.
.. _`issue 26632`: http://bugs.python.org/issue26632
.. _builtins: https://docs.python.org/3/library/builtins.html
.. _`directly controls`: https://docs.python.org/3/tutorial/modules.html#importing-from-a-package
.. _`actively prohibited`: http://pep8.readthedocs.io/en/latest/intro.html?highlight=e402#error-codes
.. _`out of sync`: http://bugs.python.org/issue23883
.. _Other: https://pypi.python.org/pypi/public
.. _implementations: http://bugs.python.org/issue22247#msg225637
.. _Sphinx: http://www.sphinx-doc.org/en/stable/
|