PageRenderTime 64ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/astropy/io/registry/core.py

https://github.com/astropy/astropy
Python | 404 lines | 373 code | 16 blank | 15 comment | 17 complexity | 5dc4d8a56721ede91c5b728ae74d3168 MD5 | raw file
  1. # Licensed under a 3-clause BSD style license - see LICENSE.rst
  2. import os
  3. import sys
  4. from collections import OrderedDict
  5. import numpy as np
  6. from .base import IORegistryError, _UnifiedIORegistryBase
  7. __all__ = ['UnifiedIORegistry', 'UnifiedInputRegistry', 'UnifiedOutputRegistry']
  8. PATH_TYPES = (str, os.PathLike) # TODO! include bytes
  9. def _expand_user_in_args(args):
  10. # Conservatively attempt to apply `os.path.expanduser` to the first
  11. # argument, which can be either a path or the contents of a table.
  12. if len(args) and isinstance(args[0], PATH_TYPES):
  13. ex_user = os.path.expanduser(args[0])
  14. if ex_user != args[0] and os.path.exists(os.path.dirname(ex_user)):
  15. args = (ex_user,) + args[1:]
  16. return args
  17. # -----------------------------------------------------------------------------
  18. class UnifiedInputRegistry(_UnifiedIORegistryBase):
  19. """Read-only Unified Registry.
  20. .. versionadded:: 5.0
  21. Examples
  22. --------
  23. First let's start by creating a read-only registry.
  24. .. code-block:: python
  25. >>> from astropy.io.registry import UnifiedInputRegistry
  26. >>> read_reg = UnifiedInputRegistry()
  27. There is nothing in this registry. Let's make a reader for the
  28. :class:`~astropy.table.Table` class::
  29. from astropy.table import Table
  30. def my_table_reader(filename, some_option=1):
  31. # Read in the table by any means necessary
  32. return table # should be an instance of Table
  33. Such a function can then be registered with the I/O registry::
  34. read_reg.register_reader('my-table-format', Table, my_table_reader)
  35. Note that we CANNOT then read in a table with::
  36. d = Table.read('my_table_file.mtf', format='my-table-format')
  37. Why? because ``Table.read`` uses Astropy's default global registry and this
  38. is a separate registry.
  39. Instead we can read by the read method on the registry::
  40. d = read_reg.read(Table, 'my_table_file.mtf', format='my-table-format')
  41. """
  42. def __init__(self):
  43. super().__init__() # set _identifiers
  44. self._readers = OrderedDict()
  45. self._registries["read"] = dict(attr="_readers", column="Read")
  46. self._registries_order = ("read", "identify")
  47. # =========================================================================
  48. # Read methods
  49. def register_reader(self, data_format, data_class, function, force=False,
  50. priority=0):
  51. """
  52. Register a reader function.
  53. Parameters
  54. ----------
  55. data_format : str
  56. The data format identifier. This is the string that will be used to
  57. specify the data type when reading.
  58. data_class : class
  59. The class of the object that the reader produces.
  60. function : function
  61. The function to read in a data object.
  62. force : bool, optional
  63. Whether to override any existing function if already present.
  64. Default is ``False``.
  65. priority : int, optional
  66. The priority of the reader, used to compare possible formats when
  67. trying to determine the best reader to use. Higher priorities are
  68. preferred over lower priorities, with the default priority being 0
  69. (negative numbers are allowed though).
  70. """
  71. if not (data_format, data_class) in self._readers or force:
  72. self._readers[(data_format, data_class)] = function, priority
  73. else:
  74. raise IORegistryError("Reader for format '{}' and class '{}' is "
  75. 'already defined'
  76. ''.format(data_format, data_class.__name__))
  77. if data_class not in self._delayed_docs_classes:
  78. self._update__doc__(data_class, 'read')
  79. def unregister_reader(self, data_format, data_class):
  80. """
  81. Unregister a reader function
  82. Parameters
  83. ----------
  84. data_format : str
  85. The data format identifier.
  86. data_class : class
  87. The class of the object that the reader produces.
  88. """
  89. if (data_format, data_class) in self._readers:
  90. self._readers.pop((data_format, data_class))
  91. else:
  92. raise IORegistryError("No reader defined for format '{}' and class '{}'"
  93. ''.format(data_format, data_class.__name__))
  94. if data_class not in self._delayed_docs_classes:
  95. self._update__doc__(data_class, 'read')
  96. def get_reader(self, data_format, data_class):
  97. """Get reader for ``data_format``.
  98. Parameters
  99. ----------
  100. data_format : str
  101. The data format identifier. This is the string that is used to
  102. specify the data type when reading/writing.
  103. data_class : class
  104. The class of the object that can be written.
  105. Returns
  106. -------
  107. reader : callable
  108. The registered reader function for this format and class.
  109. """
  110. readers = [(fmt, cls) for fmt, cls in self._readers if fmt == data_format]
  111. for reader_format, reader_class in readers:
  112. if self._is_best_match(data_class, reader_class, readers):
  113. return self._readers[(reader_format, reader_class)][0]
  114. else:
  115. format_table_str = self._get_format_table_str(data_class, 'Read')
  116. raise IORegistryError(
  117. "No reader defined for format '{}' and class '{}'.\n\nThe "
  118. "available formats are:\n\n{}".format(
  119. data_format, data_class.__name__, format_table_str))
  120. def read(self, cls, *args, format=None, cache=False, **kwargs):
  121. """
  122. Read in data.
  123. Parameters
  124. ----------
  125. cls : class
  126. *args
  127. The arguments passed to this method depend on the format.
  128. format : str or None
  129. cache : bool
  130. Whether to cache the results of reading in the data.
  131. **kwargs
  132. The arguments passed to this method depend on the format.
  133. Returns
  134. -------
  135. object or None
  136. The output of the registered reader.
  137. """
  138. ctx = None
  139. try:
  140. # Expand a tilde-prefixed path if present in args[0]
  141. args = _expand_user_in_args(args)
  142. if format is None:
  143. path = None
  144. fileobj = None
  145. if len(args):
  146. if isinstance(args[0], PATH_TYPES) and not os.path.isdir(args[0]):
  147. from astropy.utils.data import get_readable_fileobj
  148. # path might be a os.PathLike object
  149. if isinstance(args[0], os.PathLike):
  150. args = (os.fspath(args[0]),) + args[1:]
  151. path = args[0]
  152. try:
  153. ctx = get_readable_fileobj(args[0], encoding='binary', cache=cache)
  154. fileobj = ctx.__enter__()
  155. except OSError:
  156. raise
  157. except Exception:
  158. fileobj = None
  159. else:
  160. args = [fileobj] + list(args[1:])
  161. elif hasattr(args[0], 'read'):
  162. path = None
  163. fileobj = args[0]
  164. format = self._get_valid_format(
  165. 'read', cls, path, fileobj, args, kwargs)
  166. reader = self.get_reader(format, cls)
  167. data = reader(*args, **kwargs)
  168. if not isinstance(data, cls):
  169. # User has read with a subclass where only the parent class is
  170. # registered. This returns the parent class, so try coercing
  171. # to desired subclass.
  172. try:
  173. data = cls(data)
  174. except Exception:
  175. raise TypeError('could not convert reader output to {} '
  176. 'class.'.format(cls.__name__))
  177. finally:
  178. if ctx is not None:
  179. ctx.__exit__(*sys.exc_info())
  180. return data
  181. # -----------------------------------------------------------------------------
  182. class UnifiedOutputRegistry(_UnifiedIORegistryBase):
  183. """Write-only Registry.
  184. .. versionadded:: 5.0
  185. """
  186. def __init__(self):
  187. super().__init__()
  188. self._writers = OrderedDict()
  189. self._registries["write"] = dict(attr="_writers", column="Write")
  190. self._registries_order = ("write", "identify", )
  191. # =========================================================================
  192. # Write Methods
  193. def register_writer(self, data_format, data_class, function, force=False, priority=0):
  194. """
  195. Register a table writer function.
  196. Parameters
  197. ----------
  198. data_format : str
  199. The data format identifier. This is the string that will be used to
  200. specify the data type when writing.
  201. data_class : class
  202. The class of the object that can be written.
  203. function : function
  204. The function to write out a data object.
  205. force : bool, optional
  206. Whether to override any existing function if already present.
  207. Default is ``False``.
  208. priority : int, optional
  209. The priority of the writer, used to compare possible formats when trying
  210. to determine the best writer to use. Higher priorities are preferred
  211. over lower priorities, with the default priority being 0 (negative
  212. numbers are allowed though).
  213. """
  214. if not (data_format, data_class) in self._writers or force:
  215. self._writers[(data_format, data_class)] = function, priority
  216. else:
  217. raise IORegistryError("Writer for format '{}' and class '{}' is "
  218. 'already defined'
  219. ''.format(data_format, data_class.__name__))
  220. if data_class not in self._delayed_docs_classes:
  221. self._update__doc__(data_class, 'write')
  222. def unregister_writer(self, data_format, data_class):
  223. """
  224. Unregister a writer function
  225. Parameters
  226. ----------
  227. data_format : str
  228. The data format identifier.
  229. data_class : class
  230. The class of the object that can be written.
  231. """
  232. if (data_format, data_class) in self._writers:
  233. self._writers.pop((data_format, data_class))
  234. else:
  235. raise IORegistryError("No writer defined for format '{}' and class '{}'"
  236. ''.format(data_format, data_class.__name__))
  237. if data_class not in self._delayed_docs_classes:
  238. self._update__doc__(data_class, 'write')
  239. def get_writer(self, data_format, data_class):
  240. """Get writer for ``data_format``.
  241. Parameters
  242. ----------
  243. data_format : str
  244. The data format identifier. This is the string that is used to
  245. specify the data type when reading/writing.
  246. data_class : class
  247. The class of the object that can be written.
  248. Returns
  249. -------
  250. writer : callable
  251. The registered writer function for this format and class.
  252. """
  253. writers = [(fmt, cls) for fmt, cls in self._writers if fmt == data_format]
  254. for writer_format, writer_class in writers:
  255. if self._is_best_match(data_class, writer_class, writers):
  256. return self._writers[(writer_format, writer_class)][0]
  257. else:
  258. format_table_str = self._get_format_table_str(data_class, 'Write')
  259. raise IORegistryError(
  260. "No writer defined for format '{}' and class '{}'.\n\nThe "
  261. "available formats are:\n\n{}".format(
  262. data_format, data_class.__name__, format_table_str))
  263. def write(self, data, *args, format=None, **kwargs):
  264. """
  265. Write out data.
  266. Parameters
  267. ----------
  268. data : object
  269. The data to write.
  270. *args
  271. The arguments passed to this method depend on the format.
  272. format : str or None
  273. **kwargs
  274. The arguments passed to this method depend on the format.
  275. Returns
  276. -------
  277. object or None
  278. The output of the registered writer. Most often `None`.
  279. .. versionadded:: 4.3
  280. """
  281. # Expand a tilde-prefixed path if present in args[0]
  282. args = _expand_user_in_args(args)
  283. if format is None:
  284. path = None
  285. fileobj = None
  286. if len(args):
  287. if isinstance(args[0], PATH_TYPES):
  288. # path might be a os.PathLike object
  289. if isinstance(args[0], os.PathLike):
  290. args = (os.fspath(args[0]),) + args[1:]
  291. path = args[0]
  292. fileobj = None
  293. elif hasattr(args[0], 'read'):
  294. path = None
  295. fileobj = args[0]
  296. format = self._get_valid_format(
  297. 'write', data.__class__, path, fileobj, args, kwargs)
  298. writer = self.get_writer(format, data.__class__)
  299. return writer(data, *args, **kwargs)
  300. # -----------------------------------------------------------------------------
  301. class UnifiedIORegistry(UnifiedInputRegistry, UnifiedOutputRegistry):
  302. """Unified I/O Registry.
  303. .. versionadded:: 5.0
  304. """
  305. def __init__(self):
  306. super().__init__()
  307. self._registries_order = ("read", "write", "identify")
  308. def get_formats(self, data_class=None, readwrite=None):
  309. """
  310. Get the list of registered I/O formats as a `~astropy.table.Table`.
  311. Parameters
  312. ----------
  313. data_class : class, optional
  314. Filter readers/writer to match data class (default = all classes).
  315. readwrite : str or None, optional
  316. Search only for readers (``"Read"``) or writers (``"Write"``).
  317. If None search for both. Default is None.
  318. .. versionadded:: 1.3
  319. Returns
  320. -------
  321. format_table : :class:`~astropy.table.Table`
  322. Table of available I/O formats.
  323. """
  324. return super().get_formats(data_class, readwrite)