@@ -19,17 +19,14 @@ As of the time of writing, there is no *great* way to handle this issue in
19
19
the Python packaging ecosystem, but there are a few options that might be
20
20
better than nothing:
21
21
22
+ Detecting missing extras
23
+ ========================
22
24
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.
30
27
31
- Handling failing imports
32
- ========================
28
+ Trying to import and handling failure
29
+ -------------------------------------
33
30
34
31
The perhaps simplest option, which is also in line with the :term: `EAFP `
35
32
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
48
45
requirement than specified by your extra).
49
46
50
47
51
- Using ``pkg_resources `` (deprecated)
52
- ====================================
48
+ Using ``pkg_resources ``
49
+ -----------------------
53
50
54
51
The now-deprecated :ref: `pkg_resources <ResourceManager API >` package (part of
55
52
the ``setuptools `` distribution) provides a ``require `` function that you can
@@ -77,7 +74,7 @@ Unfortunately, no drop-in replacement for this functionality exists in
77
74
78
75
79
76
Using 3rd-party libraries
80
- =========================
77
+ -------------------------
81
78
82
79
In response to the aforementioned lack of a replacement for
83
80
``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/>`_
87
84
package as ``hbutils.system.check_reqs ``.
88
85
89
86
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
+
90
210
------------------
91
211
92
212
.. _packaging-problems-317 : https://github.com/pypa/packaging-problems/issues/317
0 commit comments