Skip to content

Commit f8ba7b1

Browse files
committed
Add sections on how to handle missing extra
1 parent a792059 commit f8ba7b1

File tree

1 file changed

+132
-12
lines changed

1 file changed

+132
-12
lines changed

source/guides/handling-missing-extras-at-runtime.rst

Lines changed: 132 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,14 @@ As of the time of writing, there is no *great* way to handle this issue in
1919
the Python packaging ecosystem, but there are a few options that might be
2020
better than nothing:
2121

22+
Detecting missing extras
23+
========================
2224

23-
Overall approach
24-
================
25-
26-
TODO General guidance about how to isolate imports in question
27-
28-
TODO Optimistic vs pessimistic handling?
29-
25+
We first consider how to *detect* if an extra is missing, leaving what to do
26+
about it for the next section.
3027

31-
Handling failing imports
32-
========================
28+
Trying to import and handling failure
29+
-------------------------------------
3330

3431
The perhaps simplest option, which is also in line with the :term:`EAFP`
3532
principle, is to just import your optional dependency modules as normal and
@@ -48,8 +45,8 @@ because another installed package depends on it with a wider version
4845
requirement than specified by your extra).
4946

5047

51-
Using ``pkg_resources`` (deprecated)
52-
====================================
48+
Using ``pkg_resources``
49+
-----------------------
5350

5451
The now-deprecated :ref:`pkg_resources <ResourceManager API>` package (part of
5552
the ``setuptools`` distribution) provides a ``require`` function that you can
@@ -77,7 +74,7 @@ Unfortunately, no drop-in replacement for this functionality exists in
7774

7875

7976
Using 3rd-party libraries
80-
=========================
77+
-------------------------
8178

8279
In response to the aforementioned lack of a replacement for
8380
``pkg_resources.require``, at least one 3rd party implementation of this
@@ -87,6 +84,129 @@ made available in the 3rd-party `hbutils <https://pypi.org/project/hbutils/>`_
8784
package as ``hbutils.system.check_reqs``.
8885

8986

87+
Handling missing extras
88+
=======================
89+
90+
In each of the previous section's code snippets, we omitted what to actually do
91+
when a missing extra has been identified.
92+
93+
The sensible answers to this questions are intimately linked to *where* in the
94+
code the missing extra detection and import of the optional dependencies should
95+
be performed, so we will look at our options for that as well.
96+
97+
Import at module level, raise exception
98+
---------------------------------------
99+
100+
If your package is a library and the feature that requires the extra is
101+
localized to a specific module or sub-package of your package, one option is to
102+
just raise a custom exception indicating which extra would be required:
103+
104+
.. code-block:: python
105+
106+
@dataclass
107+
class MissingExtra(Exception):
108+
name: str
109+
110+
...
111+
112+
# if extra not installed (see previous sections):
113+
raise MissingExtra("your-extra")
114+
115+
Library consumers will then have to either depend on your library with the
116+
extra enabled or handle the possibility that imports of this specific module
117+
fail (putting them in the same situation you were in). Because imports raising
118+
custom exceptions is highly unusual, you should make sure to document this in a
119+
**very** visible manner.
120+
121+
If your package is an application, making *you* the module's consumer, and you
122+
want the application to work without the extra installed (i.e. the extra only
123+
provides optional functionality for the application), you've similarly "pushed"
124+
the problem of dealing with failing imports up one layer. At some point in the
125+
module dependency you'll have to switch to a different strategy, lest your
126+
application just crash with an exception on startup.
127+
128+
129+
Import at module level, replace with exception-raising dummies
130+
--------------------------------------------------------------
131+
132+
An alternative is to delay raising the exception until an actual attempt is
133+
made to *use* the missing dependency. One way to do this is to assign "dummy"
134+
functions that do nothing but raise it to the would-be imported names in the
135+
event that the extra is missing:
136+
137+
.. code-block:: python
138+
139+
# if extra installed (see previous sections):
140+
import some_function from optional_dependency
141+
142+
...
143+
144+
# if extra not installed (see previous sections):
145+
def raise_missing_extra(*args, **kwargs):
146+
raise MissingExtra("your-extra")
147+
148+
optional_dependency = raise_missing_extra
149+
150+
Note that, if imports are not mere functions but also objects like classes that
151+
are subclassed from, the except shape of the dummy objects can get more
152+
involved depending on the expected usage, e.g.
153+
154+
.. code-block:: python
155+
156+
class RaiseMissingExtra:
157+
def __init__(self, *args, **kwargs):
158+
raise MissingExtra("your-extra")
159+
160+
which would in turn not be sufficient for a class with class methods that might
161+
be used without instantiating it, and so on.
162+
163+
By delaying the exception until attempted usage, an application installed
164+
without the extra can start and run normally until the user tries to use
165+
functionality requiring the extra, at which point you can handle it (e.g.
166+
display an appropriate error message).
167+
168+
TODO mention that 3rd party library that does this automatically
169+
170+
Import at function/method level, raise exception
171+
------------------------------------------------
172+
173+
Lastly, another way to delay exception raising until actual usage is to only
174+
perform the check for whether the extra is installed and the corresponding
175+
import when the functionality requiring it is actually used. E.g.:
176+
177+
.. code-block:: python
178+
179+
def import_extra_func_if_avail():
180+
# surround this with the appropriate checks / error handling:
181+
...
182+
from your_optional_dependency import extra_func
183+
...
184+
185+
return extra_func
186+
187+
...
188+
189+
def some_func_requiring_your_extra():
190+
try:
191+
some_function = import_extra_func_if_avail()
192+
except MissingExtra:
193+
... # handle missing extra
194+
195+
While this solution is more robust than the one from the preceding subsection,
196+
it can take more effort to make it work with static type checking.
197+
198+
Interaction with static type checking
199+
=====================================
200+
201+
TODO either put here or directly in previous sections... not sure
202+
203+
Other considerations
204+
====================
205+
206+
TODO mention that you might want to provide a way for users to check
207+
availability without performing another action for the last 2 methods
208+
209+
90210
------------------
91211

92212
.. _packaging-problems-317: https://github.com/pypa/packaging-problems/issues/317

0 commit comments

Comments
 (0)