1# -*- coding: utf-8 -*-
2# imageio is distributed under the terms of the (new) BSD License.
3
4"""
5
6.. note::
7 imageio is under construction, some details with regard to the
8 Reader and Writer classes may change.
9
10These are the main classes of imageio. They expose an interface for
11advanced users and plugin developers. A brief overview:
12
13 * imageio.FormatManager - for keeping track of registered formats.
14 * imageio.Format - representation of a file format reader/writer
15 * imageio.Format.Reader - object used during the reading of a file.
16 * imageio.Format.Writer - object used during saving a file.
17 * imageio.Request - used to store the filename and other info.
18
19Plugins need to implement a Format class and register
20a format object using ``imageio.formats.add_format()``.
21
22"""
23
24# todo: do we even use the known extensions?
25
26# Some notes:
27#
28# The classes in this module use the Request object to pass filename and
29# related info around. This request object is instantiated in
30# imageio.get_reader and imageio.get_writer.
31
32import sys
33import warnings
34import contextlib
35
36import numpy as np
37from pathlib import Path
38
39from . import Array, asarray
40from .request import ImageMode
41from ..config import known_plugins, known_extensions, PluginConfig, FileExtension
42from ..config.plugins import _original_order
43from .imopen import imopen
44
45
46# survived for backwards compatibility
47# I don't know if external plugin code depends on it existing
48# We no longer do
49MODENAMES = ImageMode
50
51
52def _get_config(plugin):
53 """Old Plugin resolution logic.
54
55 Remove once we remove the old format manager.
56 """
57
58 extension_name = None
59
60 if Path(plugin).suffix.lower() in known_extensions:
61 extension_name = Path(plugin).suffix.lower()
62 elif plugin in known_plugins:
63 pass
64 elif plugin.lower() in known_extensions:
65 extension_name = plugin.lower()
66 elif "." + plugin.lower() in known_extensions:
67 extension_name = "." + plugin.lower()
68 else:
69 raise IndexError(f"No format known by name `{plugin}`.")
70
71 if extension_name is not None:
72 for plugin_name in [
73 x
74 for file_extension in known_extensions[extension_name]
75 for x in file_extension.priority
76 ]:
77 if known_plugins[plugin_name].is_legacy:
78 plugin = plugin_name
79 break
80
81 return known_plugins[plugin]
82
83
84class Format(object):
85 """Represents an implementation to read/write a particular file format
86
87 A format instance is responsible for 1) providing information about
88 a format; 2) determining whether a certain file can be read/written
89 with this format; 3) providing a reader/writer class.
90
91 Generally, imageio will select the right format and use that to
92 read/write an image. A format can also be explicitly chosen in all
93 read/write functions. Use ``print(format)``, or ``help(format_name)``
94 to see its documentation.
95
96 To implement a specific format, one should create a subclass of
97 Format and the Format.Reader and Format.Writer classes. See
98 :class:`imageio.plugins` for details.
99
100 Parameters
101 ----------
102 name : str
103 A short name of this format. Users can select a format using its name.
104 description : str
105 A one-line description of the format.
106 extensions : str | list | None
107 List of filename extensions that this format supports. If a
108 string is passed it should be space or comma separated. The
109 extensions are used in the documentation and to allow users to
110 select a format by file extension. It is not used to determine
111 what format to use for reading/saving a file.
112 modes : str
113 A string containing the modes that this format can handle ('iIvV'),
114 “i” for an image, “I” for multiple images, “v” for a volume,
115 “V” for multiple volumes.
116 This attribute is used in the documentation and to select the
117 formats when reading/saving a file.
118 """
119
120 def __init__(self, name, description, extensions=None, modes=None):
121 """Initialize the Plugin.
122
123 Parameters
124 ----------
125 name : str
126 A short name of this format. Users can select a format using its name.
127 description : str
128 A one-line description of the format.
129 extensions : str | list | None
130 List of filename extensions that this format supports. If a
131 string is passed it should be space or comma separated. The
132 extensions are used in the documentation and to allow users to
133 select a format by file extension. It is not used to determine
134 what format to use for reading/saving a file.
135 modes : str
136 A string containing the modes that this format can handle ('iIvV'),
137 “i” for an image, “I” for multiple images, “v” for a volume,
138 “V” for multiple volumes.
139 This attribute is used in the documentation and to select the
140 formats when reading/saving a file.
141 """
142
143 # Store name and description
144 self._name = name.upper()
145 self._description = description
146
147 # Store extensions, do some effort to normalize them.
148 # They are stored as a list of lowercase strings without leading dots.
149 if extensions is None:
150 extensions = []
151 elif isinstance(extensions, str):
152 extensions = extensions.replace(",", " ").split(" ")
153 #
154 if isinstance(extensions, (tuple, list)):
155 self._extensions = tuple(
156 ["." + e.strip(".").lower() for e in extensions if e]
157 )
158 else:
159 raise ValueError("Invalid value for extensions given.")
160
161 # Store mode
162 self._modes = modes or ""
163 if not isinstance(self._modes, str):
164 raise ValueError("Invalid value for modes given.")
165 for m in self._modes:
166 if m not in "iIvV?":
167 raise ValueError("Invalid value for mode given.")
168
169 def __repr__(self):
170 # Short description
171 return "<Format %s - %s>" % (self.name, self.description)
172
173 def __str__(self):
174 return self.doc
175
176 @property
177 def doc(self):
178 """The documentation for this format (name + description + docstring)."""
179 # Our docsring is assumed to be indented by four spaces. The
180 # first line needs special attention.
181 return "%s - %s\n\n %s\n" % (
182 self.name,
183 self.description,
184 self.__doc__.strip(),
185 )
186
187 @property
188 def name(self):
189 """The name of this format."""
190 return self._name
191
192 @property
193 def description(self):
194 """A short description of this format."""
195 return self._description
196
197 @property
198 def extensions(self):
199 """A list of file extensions supported by this plugin.
200 These are all lowercase with a leading dot.
201 """
202 return self._extensions
203
204 @property
205 def modes(self):
206 """A string specifying the modes that this format can handle."""
207 return self._modes
208
209 def get_reader(self, request):
210 """get_reader(request)
211
212 Return a reader object that can be used to read data and info
213 from the given file. Users are encouraged to use
214 imageio.get_reader() instead.
215 """
216 select_mode = request.mode[1] if request.mode[1] in "iIvV" else ""
217 if select_mode not in self.modes:
218 raise RuntimeError(
219 f"Format {self.name} cannot read in {request.mode.image_mode} mode"
220 )
221 return self.Reader(self, request)
222
223 def get_writer(self, request):
224 """get_writer(request)
225
226 Return a writer object that can be used to write data and info
227 to the given file. Users are encouraged to use
228 imageio.get_writer() instead.
229 """
230 select_mode = request.mode[1] if request.mode[1] in "iIvV" else ""
231 if select_mode not in self.modes:
232 raise RuntimeError(
233 f"Format {self.name} cannot write in {request.mode.image_mode} mode"
234 )
235 return self.Writer(self, request)
236
237 def can_read(self, request):
238 """can_read(request)
239
240 Get whether this format can read data from the specified uri.
241 """
242 return self._can_read(request)
243
244 def can_write(self, request):
245 """can_write(request)
246
247 Get whether this format can write data to the speciefed uri.
248 """
249 return self._can_write(request)
250
251 def _can_read(self, request): # pragma: no cover
252 """Check if Plugin can read from ImageResource.
253
254 This method is called when the format manager is searching for a format
255 to read a certain image. Return True if this format can do it.
256
257 The format manager is aware of the extensions and the modes that each
258 format can handle. It will first ask all formats that *seem* to be able
259 to read it whether they can. If none can, it will ask the remaining
260 formats if they can: the extension might be missing, and this allows
261 formats to provide functionality for certain extensions, while giving
262 preference to other plugins.
263
264 If a format says it can, it should live up to it. The format would
265 ideally check the request.firstbytes and look for a header of some kind.
266
267 Parameters
268 ----------
269 request : Request
270 A request that can be used to access the ImageResource and obtain
271 metadata about it.
272
273 Returns
274 -------
275 can_read : bool
276 True if the plugin can read from the ImageResource, False otherwise.
277
278 """
279 return None # Plugins must implement this
280
281 def _can_write(self, request): # pragma: no cover
282 """Check if Plugin can write to ImageResource.
283
284 Parameters
285 ----------
286 request : Request
287 A request that can be used to access the ImageResource and obtain
288 metadata about it.
289
290 Returns
291 -------
292 can_read : bool
293 True if the plugin can write to the ImageResource, False otherwise.
294
295 """
296 return None # Plugins must implement this
297
298 # -----
299
300 class _BaseReaderWriter(object):
301 """Base class for the Reader and Writer class to implement common
302 functionality. It implements a similar approach for opening/closing
303 and context management as Python's file objects.
304 """
305
306 def __init__(self, format, request):
307 self.__closed = False
308 self._BaseReaderWriter_last_index = -1
309 self._format = format
310 self._request = request
311 # Open the reader/writer
312 self._open(**self.request.kwargs.copy())
313
314 @property
315 def format(self):
316 """The :class:`.Format` object corresponding to the current
317 read/write operation.
318 """
319 return self._format
320
321 @property
322 def request(self):
323 """The :class:`.Request` object corresponding to the
324 current read/write operation.
325 """
326 return self._request
327
328 def __enter__(self):
329 self._checkClosed()
330 return self
331
332 def __exit__(self, type, value, traceback):
333 if value is None:
334 # Otherwise error in close hide the real error.
335 self.close()
336
337 def __del__(self):
338 try:
339 self.close()
340 except Exception: # pragma: no cover
341 pass # Suppress noise when called during interpreter shutdown
342
343 def close(self):
344 """Flush and close the reader/writer.
345 This method has no effect if it is already closed.
346 """
347 if self.__closed:
348 return
349 self.__closed = True
350 self._close()
351 # Process results and clean request object
352 self.request.finish()
353
354 @property
355 def closed(self):
356 """Whether the reader/writer is closed."""
357 return self.__closed
358
359 def _checkClosed(self, msg=None):
360 """Internal: raise an ValueError if reader/writer is closed"""
361 if self.closed:
362 what = self.__class__.__name__
363 msg = msg or ("I/O operation on closed %s." % what)
364 raise RuntimeError(msg)
365
366 # To implement
367
368 def _open(self, **kwargs):
369 """_open(**kwargs)
370
371 Plugins should probably implement this.
372
373 It is called when reader/writer is created. Here the
374 plugin can do its initialization. The given keyword arguments
375 are those that were given by the user at imageio.read() or
376 imageio.write().
377 """
378 raise NotImplementedError()
379
380 def _close(self):
381 """_close()
382
383 Plugins should probably implement this.
384
385 It is called when the reader/writer is closed. Here the plugin
386 can do a cleanup, flush, etc.
387
388 """
389 raise NotImplementedError()
390
391 # -----
392
393 class Reader(_BaseReaderWriter):
394 """
395 The purpose of a reader object is to read data from an image
396 resource, and should be obtained by calling :func:`.get_reader`.
397
398 A reader can be used as an iterator to read multiple images,
399 and (if the format permits) only reads data from the file when
400 new data is requested (i.e. streaming). A reader can also be
401 used as a context manager so that it is automatically closed.
402
403 Plugins implement Reader's for different formats. Though rare,
404 plugins may provide additional functionality (beyond what is
405 provided by the base reader class).
406 """
407
408 def get_length(self):
409 """get_length()
410
411 Get the number of images in the file. (Note: you can also
412 use ``len(reader_object)``.)
413
414 The result can be:
415 * 0 for files that only have meta data
416 * 1 for singleton images (e.g. in PNG, JPEG, etc.)
417 * N for image series
418 * inf for streams (series of unknown length)
419 """
420 return self._get_length()
421
422 def get_data(self, index, **kwargs):
423 """get_data(index, **kwargs)
424
425 Read image data from the file, using the image index. The
426 returned image has a 'meta' attribute with the meta data.
427 Raises IndexError if the index is out of range.
428
429 Some formats may support additional keyword arguments. These are
430 listed in the documentation of those formats.
431 """
432 self._checkClosed()
433 self._BaseReaderWriter_last_index = index
434 try:
435 im, meta = self._get_data(index, **kwargs)
436 except StopIteration:
437 raise IndexError(index)
438 return Array(im, meta) # Array tests im and meta
439
440 def get_next_data(self, **kwargs):
441 """get_next_data(**kwargs)
442
443 Read the next image from the series.
444
445 Some formats may support additional keyword arguments. These are
446 listed in the documentation of those formats.
447 """
448 return self.get_data(self._BaseReaderWriter_last_index + 1, **kwargs)
449
450 def set_image_index(self, index, **kwargs):
451 """set_image_index(index)
452
453 Set the internal pointer such that the next call to
454 get_next_data() returns the image specified by the index
455 """
456 self._checkClosed()
457 n = self.get_length()
458 self._BaseReaderWriter_last_index = min(max(index - 1, -1), n)
459
460 def get_meta_data(self, index=None):
461 """get_meta_data(index=None)
462
463 Read meta data from the file. using the image index. If the
464 index is omitted or None, return the file's (global) meta data.
465
466 Note that ``get_data`` also provides the meta data for the returned
467 image as an attribute of that image.
468
469 The meta data is a dict, which shape depends on the format.
470 E.g. for JPEG, the dict maps group names to subdicts and each
471 group is a dict with name-value pairs. The groups represent
472 the different metadata formats (EXIF, XMP, etc.).
473 """
474 self._checkClosed()
475 meta = self._get_meta_data(index)
476 if not isinstance(meta, dict):
477 raise ValueError(
478 "Meta data must be a dict, not %r" % meta.__class__.__name__
479 )
480 return meta
481
482 def iter_data(self):
483 """iter_data()
484
485 Iterate over all images in the series. (Note: you can also
486 iterate over the reader object.)
487
488 """
489 self._checkClosed()
490 n = self.get_length()
491 i = 0
492 while i < n:
493 try:
494 im, meta = self._get_data(i)
495 except StopIteration:
496 return
497 except IndexError:
498 if n == float("inf"):
499 return
500 raise
501 yield Array(im, meta)
502 i += 1
503
504 # Compatibility
505
506 def __iter__(self):
507 return self.iter_data()
508
509 def __len__(self):
510 n = self.get_length()
511 if n == float("inf"):
512 n = sys.maxsize
513 return n
514
515 # To implement
516
517 def _get_length(self):
518 """_get_length()
519
520 Plugins must implement this.
521
522 The returned scalar specifies the number of images in the series.
523 See Reader.get_length for more information.
524 """
525 raise NotImplementedError()
526
527 def _get_data(self, index):
528 """_get_data()
529
530 Plugins must implement this, but may raise an IndexError in
531 case the plugin does not support random access.
532
533 It should return the image and meta data: (ndarray, dict).
534 """
535 raise NotImplementedError()
536
537 def _get_meta_data(self, index):
538 """_get_meta_data(index)
539
540 Plugins must implement this.
541
542 It should return the meta data as a dict, corresponding to the
543 given index, or to the file's (global) meta data if index is
544 None.
545 """
546 raise NotImplementedError()
547
548 # -----
549
550 class Writer(_BaseReaderWriter):
551 """
552 The purpose of a writer object is to write data to an image
553 resource, and should be obtained by calling :func:`.get_writer`.
554
555 A writer will (if the format permits) write data to the file
556 as soon as new data is provided (i.e. streaming). A writer can
557 also be used as a context manager so that it is automatically
558 closed.
559
560 Plugins implement Writer's for different formats. Though rare,
561 plugins may provide additional functionality (beyond what is
562 provided by the base writer class).
563 """
564
565 def append_data(self, im, meta=None):
566 """append_data(im, meta={})
567
568 Append an image (and meta data) to the file. The final meta
569 data that is used consists of the meta data on the given
570 image (if applicable), updated with the given meta data.
571 """
572 self._checkClosed()
573 # Check image data
574 if not isinstance(im, np.ndarray):
575 raise ValueError("append_data requires ndarray as first arg")
576 # Get total meta dict
577 total_meta = {}
578 if hasattr(im, "meta") and isinstance(im.meta, dict):
579 total_meta.update(im.meta)
580 if meta is None:
581 pass
582 elif not isinstance(meta, dict):
583 raise ValueError("Meta must be a dict.")
584 else:
585 total_meta.update(meta)
586
587 # Decouple meta info
588 im = asarray(im)
589 # Call
590 return self._append_data(im, total_meta)
591
592 def set_meta_data(self, meta):
593 """set_meta_data(meta)
594
595 Sets the file's (global) meta data. The meta data is a dict which
596 shape depends on the format. E.g. for JPEG the dict maps
597 group names to subdicts, and each group is a dict with
598 name-value pairs. The groups represents the different
599 metadata formats (EXIF, XMP, etc.).
600
601 Note that some meta formats may not be supported for
602 writing, and individual fields may be ignored without
603 warning if they are invalid.
604 """
605 self._checkClosed()
606 if not isinstance(meta, dict):
607 raise ValueError("Meta must be a dict.")
608 else:
609 return self._set_meta_data(meta)
610
611 # To implement
612
613 def _append_data(self, im, meta):
614 # Plugins must implement this
615 raise NotImplementedError()
616
617 def _set_meta_data(self, meta):
618 # Plugins must implement this
619 raise NotImplementedError()
620
621
622class FormatManager(object):
623 """
624 The FormatManager is a singleton plugin factory.
625
626 The format manager supports getting a format object using indexing (by
627 format name or extension). When used as an iterator, this object
628 yields all registered format objects.
629
630 See also :func:`.help`.
631 """
632
633 @property
634 def _formats(self):
635 available_formats = list()
636
637 for config in known_plugins.values():
638 with contextlib.suppress(ImportError):
639 # if an exception is raised, then format not installed
640 if config.is_legacy and config.format is not None:
641 available_formats.append(config)
642
643 return available_formats
644
645 def __repr__(self):
646 return f"<imageio.FormatManager with {len(self._formats)} registered formats>"
647
648 def __iter__(self):
649 return iter(x.format for x in self._formats)
650
651 def __len__(self):
652 return len(self._formats)
653
654 def __str__(self):
655 ss = []
656 for config in self._formats:
657 ext = config.legacy_args["extensions"]
658 desc = config.legacy_args["description"]
659 s = f"{config.name} - {desc} [{ext}]"
660 ss.append(s)
661 return "\n".join(ss)
662
663 def __getitem__(self, name):
664 warnings.warn(
665 "The usage of `FormatManager` is deprecated and it will be "
666 "removed in Imageio v3. Use `iio.imopen` instead.",
667 DeprecationWarning,
668 stacklevel=2,
669 )
670
671 if not isinstance(name, str):
672 raise ValueError(
673 "Looking up a format should be done by name or by extension."
674 )
675
676 if name == "":
677 raise ValueError("No format matches the empty string.")
678
679 # Test if name is existing file
680 if Path(name).is_file():
681 # legacy compatibility - why test reading here??
682 try:
683 return imopen(name, "r", legacy_mode=True)._format
684 except ValueError:
685 # no plugin can read the file
686 pass
687
688 config = _get_config(name.upper())
689
690 try:
691 return config.format
692 except ImportError:
693 raise ImportError(
694 f"The `{config.name}` format is not installed. "
695 f"Use `pip install imageio[{config.install_name}]` to install it."
696 )
697
698 def sort(self, *names):
699 """sort(name1, name2, name3, ...)
700
701 Sort the formats based on zero or more given names; a format with
702 a name that matches one of the given names will take precedence
703 over other formats. A match means an equal name, or ending with
704 that name (though the former counts higher). Case insensitive.
705
706 Format preference will match the order of the given names: using
707 ``sort('TIFF', '-FI', '-PIL')`` would prefer the FreeImage formats
708 over the Pillow formats, but prefer TIFF even more. Each time
709 this is called, the starting point is the default format order,
710 and calling ``sort()`` with no arguments will reset the order.
711
712 Be aware that using the function can affect the behavior of
713 other code that makes use of imageio.
714
715 Also see the ``IMAGEIO_FORMAT_ORDER`` environment variable.
716 """
717
718 warnings.warn(
719 "`FormatManager` is deprecated and it will be removed in ImageIO v3."
720 " Migrating `FormatManager.sort` depends on your use-case:\n"
721 "\t- modify `iio.config.known_plugins` to specify the search order for "
722 "unrecognized formats.\n"
723 "\t- modify `iio.config.known_extensions[<extension>].priority`"
724 " to control a specific extension.",
725 DeprecationWarning,
726 stacklevel=2,
727 )
728
729 # Check and sanitize input
730 for name in names:
731 if not isinstance(name, str):
732 raise TypeError("formats.sort() accepts only string names.")
733 if any(c in name for c in ".,"):
734 raise ValueError(
735 "Names given to formats.sort() should not "
736 "contain dots `.` or commas `,`."
737 )
738
739 should_reset = len(names) == 0
740 if should_reset:
741 names = _original_order
742
743 sane_names = [name.strip().upper() for name in names if name != ""]
744
745 # enforce order for every extension that uses it
746 flat_extensions = [
747 ext for ext_list in known_extensions.values() for ext in ext_list
748 ]
749 for extension in flat_extensions:
750 if should_reset:
751 extension.reset()
752 continue
753
754 for name in reversed(sane_names):
755 for plugin in [x for x in extension.default_priority]:
756 if plugin.endswith(name):
757 extension.priority.remove(plugin)
758 extension.priority.insert(0, plugin)
759
760 old_order = known_plugins.copy()
761 known_plugins.clear()
762
763 for name in sane_names:
764 plugin = old_order.pop(name, None)
765 if plugin is not None:
766 known_plugins[name] = plugin
767
768 known_plugins.update(old_order)
769
770 def add_format(self, iio_format, overwrite=False):
771 """add_format(format, overwrite=False)
772
773 Register a format, so that imageio can use it. If a format with the
774 same name already exists, an error is raised, unless overwrite is True,
775 in which case the current format is replaced.
776 """
777
778 warnings.warn(
779 "`FormatManager` is deprecated and it will be removed in ImageIO v3."
780 "To migrate `FormatManager.add_format` add the plugin directly to "
781 "`iio.config.known_plugins`.",
782 DeprecationWarning,
783 stacklevel=2,
784 )
785
786 if not isinstance(iio_format, Format):
787 raise ValueError("add_format needs argument to be a Format object")
788 elif not overwrite and iio_format.name in self.get_format_names():
789 raise ValueError(
790 f"A Format named {iio_format.name} is already registered, use"
791 " `overwrite=True` to replace."
792 )
793
794 config = PluginConfig(
795 name=iio_format.name.upper(),
796 class_name=iio_format.__class__.__name__,
797 module_name=iio_format.__class__.__module__,
798 is_legacy=True,
799 install_name="unknown",
800 legacy_args={
801 "name": iio_format.name,
802 "description": iio_format.description,
803 "extensions": " ".join(iio_format.extensions),
804 "modes": iio_format.modes,
805 },
806 )
807
808 known_plugins[config.name] = config
809
810 for extension in iio_format.extensions:
811 # be conservative and always treat it as a unique file format
812 ext = FileExtension(
813 extension=extension,
814 priority=[config.name],
815 name="Unique Format",
816 description="A format inserted at runtime."
817 f" It is being read by the `{config.name}` plugin.",
818 )
819 known_extensions.setdefault(extension, list()).append(ext)
820
821 def search_read_format(self, request):
822 """search_read_format(request)
823
824 Search a format that can read a file according to the given request.
825 Returns None if no appropriate format was found. (used internally)
826 """
827
828 try:
829 # in legacy_mode imopen returns a LegacyPlugin
830 return imopen(request, request.mode.io_mode, legacy_mode=True)._format
831 except AttributeError:
832 warnings.warn(
833 "ImageIO now uses a v3 plugin when reading this format."
834 " Please migrate to the v3 API (preferred) or use imageio.v2.",
835 DeprecationWarning,
836 stacklevel=2,
837 )
838 return None
839 except ValueError:
840 # no plugin can read this request
841 # but the legacy API doesn't raise
842 return None
843
844 def search_write_format(self, request):
845 """search_write_format(request)
846
847 Search a format that can write a file according to the given request.
848 Returns None if no appropriate format was found. (used internally)
849 """
850
851 try:
852 # in legacy_mode imopen returns a LegacyPlugin
853 return imopen(request, request.mode.io_mode, legacy_mode=True)._format
854 except AttributeError:
855 warnings.warn(
856 "ImageIO now uses a v3 plugin when writing this format."
857 " Please migrate to the v3 API (preferred) or use imageio.v2.",
858 DeprecationWarning,
859 stacklevel=2,
860 )
861 return None
862 except ValueError:
863 # no plugin can write this request
864 # but the legacy API doesn't raise
865 return None
866
867 def get_format_names(self):
868 """Get the names of all registered formats."""
869
870 warnings.warn(
871 "`FormatManager` is deprecated and it will be removed in ImageIO v3."
872 "To migrate `FormatManager.get_format_names` use `iio.config.known_plugins.keys()` instead.",
873 DeprecationWarning,
874 stacklevel=2,
875 )
876
877 return [f.name for f in self._formats]
878
879 def show(self):
880 """Show a nicely formatted list of available formats"""
881 print(self)