manage_plugins.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. """Handle image reading, writing and plotting plugins.
  2. To improve performance, plugins are only loaded as needed. As a result, there
  3. can be multiple states for a given plugin:
  4. available: Defined in an *ini file located in ``skimage.io._plugins``.
  5. See also :func:`skimage.io.available_plugins`.
  6. partial definition: Specified in an *ini file, but not defined in the
  7. corresponding plugin module. This will raise an error when loaded.
  8. available but not on this system: Defined in ``skimage.io._plugins``, but
  9. a dependent library (e.g. Qt, PIL) is not available on your system.
  10. This will raise an error when loaded.
  11. loaded: The real availability is determined when it's explicitly loaded,
  12. either because it's one of the default plugins, or because it's
  13. loaded explicitly by the user.
  14. """
  15. import os.path
  16. import warnings
  17. from configparser import ConfigParser
  18. from glob import glob
  19. from contextlib import contextmanager
  20. from .._shared.utils import deprecate_func
  21. from .collection import imread_collection_wrapper
  22. __all__ = [
  23. 'use_plugin',
  24. 'call_plugin',
  25. 'plugin_info',
  26. 'plugin_order',
  27. 'reset_plugins',
  28. 'find_available_plugins',
  29. '_available_plugins',
  30. ]
  31. # The plugin store will save a list of *loaded* io functions for each io type
  32. # (e.g. 'imread', 'imsave', etc.). Plugins are loaded as requested.
  33. plugin_store = None
  34. # Dictionary mapping plugin names to a list of functions they provide.
  35. plugin_provides = {}
  36. # The module names for the plugins in `skimage.io._plugins`.
  37. plugin_module_name = {}
  38. # Meta-data about plugins provided by *.ini files.
  39. plugin_meta_data = {}
  40. # For each plugin type, default to the first available plugin as defined by
  41. # the following preferences.
  42. preferred_plugins = {
  43. # Default plugins for all types (overridden by specific types below).
  44. 'all': ['imageio', 'pil', 'matplotlib'],
  45. 'imshow': ['matplotlib'],
  46. 'imshow_collection': ['matplotlib'],
  47. }
  48. @contextmanager
  49. def _hide_plugin_deprecation_warnings():
  50. """Ignore warnings related to plugin infrastructure deprecation."""
  51. with warnings.catch_warnings():
  52. warnings.filterwarnings(
  53. action="ignore",
  54. message=".*use `imageio` or other I/O packages directly.*",
  55. category=FutureWarning,
  56. module="skimage",
  57. )
  58. yield
  59. def _clear_plugins():
  60. """Clear the plugin state to the default, i.e., where no plugins are loaded"""
  61. global plugin_store
  62. plugin_store = {
  63. 'imread': [],
  64. 'imsave': [],
  65. 'imshow': [],
  66. 'imread_collection': [],
  67. 'imshow_collection': [],
  68. '_app_show': [],
  69. }
  70. with _hide_plugin_deprecation_warnings():
  71. _clear_plugins()
  72. def _load_preferred_plugins():
  73. # Load preferred plugin for each io function.
  74. io_types = ['imsave', 'imshow', 'imread_collection', 'imshow_collection', 'imread']
  75. for p_type in io_types:
  76. _set_plugin(p_type, preferred_plugins['all'])
  77. plugin_types = (p for p in preferred_plugins.keys() if p != 'all')
  78. for p_type in plugin_types:
  79. _set_plugin(p_type, preferred_plugins[p_type])
  80. def _set_plugin(plugin_type, plugin_list):
  81. for plugin in plugin_list:
  82. if plugin not in _available_plugins:
  83. continue
  84. try:
  85. use_plugin(plugin, kind=plugin_type)
  86. break
  87. except (ImportError, RuntimeError, OSError):
  88. pass
  89. @deprecate_func(
  90. deprecated_version="0.25",
  91. removed_version="0.27",
  92. hint="The plugin infrastructure of `skimage.io` is deprecated. "
  93. "Instead, use `imageio` or other I/O packages directly.",
  94. )
  95. def reset_plugins():
  96. with _hide_plugin_deprecation_warnings():
  97. _clear_plugins()
  98. _load_preferred_plugins()
  99. def _parse_config_file(filename):
  100. """Return plugin name and meta-data dict from plugin config file."""
  101. parser = ConfigParser()
  102. parser.read(filename)
  103. name = parser.sections()[0]
  104. meta_data = {}
  105. for opt in parser.options(name):
  106. meta_data[opt] = parser.get(name, opt)
  107. return name, meta_data
  108. def _scan_plugins():
  109. """Scan the plugins directory for .ini files and parse them
  110. to gather plugin meta-data.
  111. """
  112. pd = os.path.dirname(__file__)
  113. config_files = glob(os.path.join(pd, '_plugins', '*.ini'))
  114. for filename in config_files:
  115. name, meta_data = _parse_config_file(filename)
  116. if 'provides' not in meta_data:
  117. warnings.warn(
  118. f'file {filename} not recognized as a scikit-image io plugin, skipping.'
  119. )
  120. continue
  121. plugin_meta_data[name] = meta_data
  122. provides = [s.strip() for s in meta_data['provides'].split(',')]
  123. valid_provides = [p for p in provides if p in plugin_store]
  124. for p in provides:
  125. if p not in plugin_store:
  126. print(f"Plugin `{name}` wants to provide non-existent `{p}`. Ignoring.")
  127. # Add plugins that provide 'imread' as provider of 'imread_collection'.
  128. need_to_add_collection = (
  129. 'imread_collection' not in valid_provides and 'imread' in valid_provides
  130. )
  131. if need_to_add_collection:
  132. valid_provides.append('imread_collection')
  133. plugin_provides[name] = valid_provides
  134. plugin_module_name[name] = os.path.basename(filename)[:-4]
  135. with _hide_plugin_deprecation_warnings():
  136. _scan_plugins()
  137. @deprecate_func(
  138. deprecated_version="0.25",
  139. removed_version="0.27",
  140. hint="The plugin infrastructure of `skimage.io` is deprecated. "
  141. "Instead, use `imageio` or other I/O packages directly.",
  142. )
  143. def find_available_plugins(loaded=False):
  144. """List available plugins.
  145. Parameters
  146. ----------
  147. loaded : bool
  148. If True, show only those plugins currently loaded. By default,
  149. all plugins are shown.
  150. Returns
  151. -------
  152. p : dict
  153. Dictionary with plugin names as keys and exposed functions as
  154. values.
  155. """
  156. active_plugins = set()
  157. for plugin_func in plugin_store.values():
  158. for plugin, func in plugin_func:
  159. active_plugins.add(plugin)
  160. d = {}
  161. for plugin in plugin_provides:
  162. if not loaded or plugin in active_plugins:
  163. d[plugin] = [f for f in plugin_provides[plugin] if not f.startswith('_')]
  164. return d
  165. with _hide_plugin_deprecation_warnings():
  166. _available_plugins = find_available_plugins()
  167. @deprecate_func(
  168. deprecated_version="0.25",
  169. removed_version="0.27",
  170. hint="The plugin infrastructure of `skimage.io` is deprecated. "
  171. "Instead, use `imageio` or other I/O packages directly.",
  172. )
  173. def call_plugin(kind, *args, **kwargs):
  174. """Find the appropriate plugin of 'kind' and execute it.
  175. Parameters
  176. ----------
  177. kind : {'imshow', 'imsave', 'imread', 'imread_collection'}
  178. Function to look up.
  179. plugin : str, optional
  180. Plugin to load. Defaults to None, in which case the first
  181. matching plugin is used.
  182. *args, **kwargs : arguments and keyword arguments
  183. Passed to the plugin function.
  184. """
  185. if kind not in plugin_store:
  186. raise ValueError(f'Invalid function ({kind}) requested.')
  187. plugin_funcs = plugin_store[kind]
  188. if len(plugin_funcs) == 0:
  189. msg = (
  190. f"No suitable plugin registered for {kind}.\n\n"
  191. "You may load I/O plugins with the `skimage.io.use_plugin` "
  192. "command. A list of all available plugins are shown in the "
  193. "`skimage.io` docstring."
  194. )
  195. raise RuntimeError(msg)
  196. plugin = kwargs.pop('plugin', None)
  197. if plugin is None:
  198. _, func = plugin_funcs[0]
  199. else:
  200. _load(plugin)
  201. try:
  202. func = [f for (p, f) in plugin_funcs if p == plugin][0]
  203. except IndexError:
  204. raise RuntimeError(f'Could not find the plugin "{plugin}" for {kind}.')
  205. return func(*args, **kwargs)
  206. @deprecate_func(
  207. deprecated_version="0.25",
  208. removed_version="0.27",
  209. hint="The plugin infrastructure of `skimage.io` is deprecated. "
  210. "Instead, use `imageio` or other I/O packages directly.",
  211. )
  212. def use_plugin(name, kind=None):
  213. """Set the default plugin for a specified operation. The plugin
  214. will be loaded if it hasn't been already.
  215. Parameters
  216. ----------
  217. name : str
  218. Name of plugin. See ``skimage.io.available_plugins`` for a list of available
  219. plugins.
  220. kind : {'imsave', 'imread', 'imshow', 'imread_collection', 'imshow_collection'}, optional
  221. Set the plugin for this function. By default,
  222. the plugin is set for all functions.
  223. Examples
  224. --------
  225. To use Matplotlib as the default image reader, you would write:
  226. >>> from skimage import io
  227. >>> io.use_plugin('matplotlib', 'imread') # doctest: +SKIP
  228. To see a list of available plugins run ``skimage.io.available_plugins``. Note
  229. that this lists plugins that are defined, but the full list may not be usable
  230. if your system does not have the required libraries installed.
  231. """
  232. if kind is None:
  233. kind = plugin_store.keys()
  234. else:
  235. if kind not in plugin_provides[name]:
  236. raise RuntimeError(f"Plugin {name} does not support `{kind}`.")
  237. if kind == 'imshow':
  238. kind = [kind, '_app_show']
  239. else:
  240. kind = [kind]
  241. _load(name)
  242. for k in kind:
  243. if k not in plugin_store:
  244. raise RuntimeError(f"'{k}' is not a known plugin function.")
  245. funcs = plugin_store[k]
  246. # Shuffle the plugins so that the requested plugin stands first
  247. # in line
  248. funcs = [(n, f) for (n, f) in funcs if n == name] + [
  249. (n, f) for (n, f) in funcs if n != name
  250. ]
  251. plugin_store[k] = funcs
  252. def _inject_imread_collection_if_needed(module):
  253. """Add `imread_collection` to module if not already present."""
  254. if not hasattr(module, 'imread_collection') and hasattr(module, 'imread'):
  255. imread = getattr(module, 'imread')
  256. func = imread_collection_wrapper(imread)
  257. setattr(module, 'imread_collection', func)
  258. @_hide_plugin_deprecation_warnings()
  259. def _load(plugin):
  260. """Load the given plugin.
  261. Parameters
  262. ----------
  263. plugin : str
  264. Name of plugin to load.
  265. See Also
  266. --------
  267. plugins : List of available plugins
  268. """
  269. if plugin in find_available_plugins(loaded=True):
  270. return
  271. if plugin not in plugin_module_name:
  272. raise ValueError(f"Plugin {plugin} not found.")
  273. else:
  274. modname = plugin_module_name[plugin]
  275. plugin_module = __import__('skimage.io._plugins.' + modname, fromlist=[modname])
  276. provides = plugin_provides[plugin]
  277. for p in provides:
  278. if p == 'imread_collection':
  279. _inject_imread_collection_if_needed(plugin_module)
  280. elif not hasattr(plugin_module, p):
  281. print(f"Plugin {plugin} does not provide {p} as advertised. Ignoring.")
  282. continue
  283. store = plugin_store[p]
  284. func = getattr(plugin_module, p)
  285. if (plugin, func) not in store:
  286. store.append((plugin, func))
  287. @deprecate_func(
  288. deprecated_version="0.25",
  289. removed_version="0.27",
  290. hint="The plugin infrastructure of `skimage.io` is deprecated. "
  291. "Instead, use `imageio` or other I/O packages directly.",
  292. )
  293. def plugin_info(plugin):
  294. """Return plugin meta-data.
  295. Parameters
  296. ----------
  297. plugin : str
  298. Name of plugin.
  299. Returns
  300. -------
  301. m : dict
  302. Meta data as specified in plugin ``.ini``.
  303. """
  304. try:
  305. return plugin_meta_data[plugin]
  306. except KeyError:
  307. raise ValueError(f'No information on plugin "{plugin}"')
  308. @deprecate_func(
  309. deprecated_version="0.25",
  310. removed_version="0.27",
  311. hint="The plugin infrastructure of `skimage.io` is deprecated. "
  312. "Instead, use `imageio` or other I/O packages directly.",
  313. )
  314. def plugin_order():
  315. """Return the currently preferred plugin order.
  316. Returns
  317. -------
  318. p : dict
  319. Dictionary of preferred plugin order, with function name as key and
  320. plugins (in order of preference) as value.
  321. """
  322. p = {}
  323. for func in plugin_store:
  324. p[func] = [plugin_name for (plugin_name, f) in plugin_store[func]]
  325. return p