@@ -141,157 +141,18 @@ similar to ``check_reqs`` but not identical:
141
141
Handling missing extras
142
142
=======================
143
143
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:
146
148
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.
150
154
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.
295
156
296
157
297
158
------------------
@@ -303,5 +164,3 @@ TODO mention that you might want to provide a way for users to check
303
164
.. _pkg_resources : https://setuptools.pypa.io/en/latest/pkg_resources.html
304
165
305
166
.. _packaging-problems-317 : https://github.com/pypa/packaging-problems/issues/317
306
-
307
- .. _generalimport : https://github.com/ManderaGeneral/generalimport
0 commit comments