Skip to content

Commit 6d0535d

Browse files
committed
Replace detailed handling w/ general idea
1 parent c109058 commit 6d0535d

File tree

1 file changed

+10
-151
lines changed

1 file changed

+10
-151
lines changed

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

Lines changed: 10 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -141,157 +141,18 @@ similar to ``check_reqs`` but not identical:
141141
Handling missing extras
142142
=======================
143143

144-
In each of the previous section's code snippets, we omitted what to actually do
145-
when a missing extra has been identified.
144+
Where and how to embed the detection of missing extras in a package and what
145+
actions to take upon learning the outcome depends on the specifics of both the
146+
package and feature requiring the extra.
147+
Some common options are:
146148

147-
The sensible answers to this questions are intimately linked to *where* in the
148-
code the missing extra detection and import of the optional dependencies should
149-
be performed, so we will look at our options for that as well.
149+
- Raise a custom exception that includes the name of the missing extra.
150+
- In applications, show an error message when an attempt is made to use the
151+
feature that requires the extra.
152+
- In libraries, provide a function that lets library consumers query which
153+
features are available.
150154

151-
Import at module level, raise exception
152-
---------------------------------------
153-
154-
If your package is a library and the feature that requires the extra is
155-
localized to a specific module or sub-package of your package, one option is to
156-
just raise a custom exception indicating which extra would be required:
157-
158-
.. code-block:: python
159-
160-
from dataclasses import dataclass
161-
162-
@dataclass
163-
class MissingExtra(Exception):
164-
name: str
165-
166-
...
167-
168-
# if extra not installed (see previous sections):
169-
raise MissingExtra("your-extra")
170-
171-
Library consumers will then have to either depend on your library with the
172-
extra enabled or handle the possibility that imports of this specific module
173-
fail (putting them in the same situation you were in). Because imports raising
174-
custom exceptions is highly unusual, you should make sure to document this in a
175-
**very** visible manner.
176-
177-
If your package is an application, making *you* the module's consumer, and you
178-
want the application to work without the extra installed (i.e. the extra only
179-
provides optional functionality for the application), you've similarly "pushed"
180-
the problem of dealing with failing imports up one layer. At some point in the
181-
module dependency you'll have to switch to a different strategy, lest your
182-
application just crash with an exception on startup.
183-
184-
185-
Import at module level, replace with exception-raising dummies
186-
--------------------------------------------------------------
187-
188-
An alternative is to delay raising the exception until an actual attempt is
189-
made to *use* the missing dependency. One way to do this is to assign "dummy"
190-
functions that do nothing but raise it to the would-be imported names in the
191-
event that the extra is missing:
192-
193-
.. code-block:: python
194-
195-
# if extra installed (see previous sections):
196-
import some_function from optional_dependency
197-
198-
...
199-
200-
# if extra not installed (see previous sections):
201-
def raise_missing_extra(*args, **kwargs):
202-
raise MissingExtra("your-extra")
203-
204-
optional_dependency = raise_missing_extra
205-
206-
Note that, if imports are not mere functions but also objects like classes that
207-
are subclassed from, the exact shape of the dummy objects can get more involved
208-
depending on the expected usage, e.g.
209-
210-
.. code-block:: python
211-
212-
class RaiseMissingExtra:
213-
def __init__(self, *args, **kwargs):
214-
raise MissingExtra("your-extra")
215-
216-
which would in turn not be sufficient for a class with class methods that might
217-
be used without instantiating it, and so on.
218-
219-
By delaying the exception until attempted usage, an application installed
220-
without the extra can start and run normally until the user tries to use
221-
functionality requiring the extra, at which point you can handle it (e.g.
222-
display an appropriate error message).
223-
224-
The `generalimport`_ library can automate this process by hooking into the
225-
import system.
226-
227-
Import at function/method level, raise exception
228-
------------------------------------------------
229-
230-
Lastly, another way to delay exception raising until actual usage is to only
231-
perform the check for whether the extra is installed and the corresponding
232-
import when the functionality requiring it is actually used. E.g.:
233-
234-
.. code-block:: python
235-
236-
def import_extra_module_if_avail():
237-
# surround this with the appropriate checks / error handling:
238-
...
239-
import your_optional_dependency
240-
...
241-
242-
return your_optional_dependency
243-
244-
...
245-
246-
def some_func_requiring_your_extra():
247-
try:
248-
optional_module = import_extra_module_if_avail()
249-
except MissingExtra:
250-
... # handle missing extra
251-
252-
# now you can use functionality from the optional dependency, e.g.:
253-
optional_module.extra_func(...)
254-
255-
While this solution is more robust than the one from the preceding subsection,
256-
it can take more effort to make it work with
257-
:term:`static type checking <static type checker>`:
258-
To correctly statically type a function returning a module, you'd have to
259-
introduce an "artificial" type representing the latter, e.g.
260-
261-
.. code-block:: python
262-
263-
from typing import cast, Protocol
264-
265-
class YourOptionalModuleType(Protocol):
266-
extra_func: Callable[...]
267-
... # other objects you want to use
268-
269-
def some_func_requiring_your_extra() -> YourOptionalModuleType:
270-
...
271-
272-
return cast(YourOptionalModuleType, optional_module)
273-
274-
An alternative would be to instead have functions that import and return only
275-
the objects you actually need:
276-
277-
.. code-block:: python
278-
279-
def import_extra_func_if_avail() -> Callable[...]:
280-
# surround this with the appropriate checks / error handling:
281-
...
282-
from your_optional_dependency import extra_func
283-
...
284-
285-
return extra_func
286-
287-
But this can become verbose when you import a lot of names.
288-
289-
290-
Other considerations
291-
====================
292-
293-
TODO mention that you might want to provide a way for users to check
294-
availability without performing another action for the last 2 methods
155+
... and probably more.
295156

296157

297158
------------------
@@ -303,5 +164,3 @@ TODO mention that you might want to provide a way for users to check
303164
.. _pkg_resources: https://setuptools.pypa.io/en/latest/pkg_resources.html
304165

305166
.. _packaging-problems-317: https://github.com/pypa/packaging-problems/issues/317
306-
307-
.. _generalimport: https://github.com/ManderaGeneral/generalimport

0 commit comments

Comments
 (0)