@@ -19,17 +19,14 @@ As of the time of writing, there is no *great* way to handle this issue in
1919the Python packaging ecosystem, but there are a few options that might be
2020better 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
3431The perhaps simplest option, which is also in line with the :term: `EAFP `
3532principle, 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
4845requirement than specified by your extra).
4946
5047
51- Using ``pkg_resources `` (deprecated)
52- ====================================
48+ Using ``pkg_resources ``
49+ -----------------------
5350
5451The now-deprecated :ref: `pkg_resources <ResourceManager API >` package (part of
5552the ``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
7976Using 3rd-party libraries
80- =========================
77+ -------------------------
8178
8279In 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/>`_
8784package 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