Coverage for tests/test_parquet.py: 98%
1349 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-09 08:55 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-09 08:55 +0000
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28"""Tests for ParquetFormatter.
30Tests in this module are disabled unless pandas and pyarrow are importable.
31"""
33import datetime
34import os
35import posixpath
36import shutil
37import unittest
38import uuid
40try:
41 import pyarrow as pa
42except ImportError:
43 pa = None
44try:
45 import astropy.table as atable
46 from astropy import units
47except ImportError:
48 atable = None
49try:
50 import numpy as np
51except ImportError:
52 np = None
53try:
54 import pandas as pd
55except ImportError:
56 pd = None
58try:
59 import boto3
60 import botocore
62 from lsst.resources.s3utils import clean_test_environment_for_s3
64 try:
65 from moto import mock_aws # v5
66 except ImportError:
67 from moto import mock_s3 as mock_aws
68except ImportError:
69 boto3 = None
71try:
72 import fsspec
73except ImportError:
74 fsspec = None
76try:
77 import s3fs
78except ImportError:
79 s3fs = None
82from lsst.daf.butler import (
83 Butler,
84 Config,
85 DatasetProvenance,
86 DatasetRef,
87 DatasetType,
88 FileDataset,
89 StorageClassConfig,
90 StorageClassFactory,
91)
92from lsst.resources import ResourcePath
94try:
95 from lsst.daf.butler.delegates.arrowtable import ArrowTableDelegate
96except ImportError:
97 pa = None
99try:
100 from lsst.daf.butler.formatters.parquet import (
101 ASTROPY_PANDAS_INDEX_KEY,
102 ArrowAstropySchema,
103 ArrowNumpySchema,
104 DataFrameSchema,
105 ParquetFormatter,
106 _append_numpy_multidim_metadata,
107 _astropy_to_numpy_dict,
108 _numpy_dict_to_numpy,
109 _numpy_dtype_to_arrow_types,
110 _numpy_style_arrays_to_arrow_arrays,
111 _numpy_to_numpy_dict,
112 add_pandas_index_to_astropy,
113 arrow_to_astropy,
114 arrow_to_numpy,
115 arrow_to_numpy_dict,
116 arrow_to_pandas,
117 astropy_to_arrow,
118 astropy_to_pandas,
119 compute_row_group_size,
120 numpy_dict_to_arrow,
121 numpy_to_arrow,
122 pandas_to_arrow,
123 pandas_to_astropy,
124 )
125except ImportError:
126 pa = None
127 pd = None
128 atable = None
129 np = None
130from lsst.daf.butler.tests.utils import makeTestTempDir, removeTestTempDir
132TESTDIR = os.path.abspath(os.path.dirname(__file__))
135def _makeSimpleNumpyTable(include_multidim=False, include_bigendian=False):
136 """Make a simple numpy table with random data.
138 Parameters
139 ----------
140 include_multidim : `bool`
141 Include multi-dimensional columns.
142 include_bigendian : `bool`
143 Include big-endian columns.
145 Returns
146 -------
147 numpyTable : `numpy.ndarray`
148 """
149 nrow = 5
151 dtype = [
152 ("index", "i4"),
153 ("a", "f8"),
154 ("b", "f8"),
155 ("c", "f8"),
156 ("ddd", "f8"),
157 ("f", "i8"),
158 ("strcol", "U10"),
159 ("bytecol", "S10"),
160 ("dtn", "datetime64[ns]"),
161 ("dtu", "datetime64[us]"),
162 ]
164 if include_multidim:
165 dtype.extend(
166 [
167 ("d1", "f4", (5,)),
168 ("d2", "i8", (5, 10)),
169 ("d3", "f8", (5, 10)),
170 ]
171 )
173 if include_bigendian:
174 dtype.extend([("a_bigendian", ">f8"), ("f_bigendian", ">i8")])
176 data = np.zeros(nrow, dtype=dtype)
177 data["index"][:] = np.arange(nrow)
178 data["a"] = np.random.randn(nrow)
179 data["b"] = np.random.randn(nrow)
180 data["c"] = np.random.randn(nrow)
181 data["ddd"] = np.random.randn(nrow)
182 data["f"] = np.arange(nrow) * 10
183 data["strcol"][:] = "teststring"
184 data["bytecol"][:] = "teststring"
185 data["dtn"] = datetime.datetime.fromisoformat("2024-07-23")
186 data["dtu"] = datetime.datetime.fromisoformat("2024-07-23")
188 if include_multidim:
189 data["d1"] = np.random.randn(data["d1"].size).reshape(data["d1"].shape)
190 data["d2"] = np.arange(data["d2"].size).reshape(data["d2"].shape)
191 data["d3"] = np.asfortranarray(np.random.randn(data["d3"].size).reshape(data["d3"].shape))
193 if include_bigendian:
194 data["a_bigendian"][:] = data["a"]
195 data["f_bigendian"][:] = data["f"]
197 return data
200def _makeSingleIndexDataFrame(include_masked=False, include_lists=False):
201 """Make a single index data frame for testing.
203 Parameters
204 ----------
205 include_masked : `bool`
206 Include masked columns.
207 include_lists : `bool`
208 Include list columns.
210 Returns
211 -------
212 dataFrame : `~pandas.DataFrame`
213 The test dataframe.
214 allColumns : `list` [`str`]
215 List of all the columns (including index columns).
216 """
217 data = _makeSimpleNumpyTable()
218 df = pd.DataFrame(data)
219 df = df.set_index("index")
221 if include_masked:
222 nrow = len(df)
224 df["m1"] = pd.array(np.arange(nrow), dtype=pd.Int64Dtype())
225 df["m2"] = pd.array(np.arange(nrow), dtype=np.float32)
226 df["mstrcol"] = pd.array(np.array(["text"] * nrow))
227 df.loc[1, ["m1", "m2", "mstrcol"]] = None
228 df.loc[0, "m1"] = 1649900760361600113
230 if include_lists:
231 nrow = len(df)
233 df["l1"] = [[0, 0]] * nrow
234 df["l2"] = [[0.0, 0.0]] * nrow
235 df["l3"] = [[]] * nrow
237 allColumns = df.columns.append(pd.Index(df.index.names))
239 return df, allColumns
242def _makeMultiIndexDataFrame():
243 """Make a multi-index data frame for testing.
245 Returns
246 -------
247 dataFrame : `~pandas.DataFrame`
248 The test dataframe.
249 """
250 columns = pd.MultiIndex.from_tuples(
251 [
252 ("g", "a"),
253 ("g", "b"),
254 ("g", "c"),
255 ("r", "a"),
256 ("r", "b"),
257 ("r", "c"),
258 ],
259 names=["filter", "column"],
260 )
261 df = pd.DataFrame(np.random.randn(5, 6), index=np.arange(5, dtype=int), columns=columns)
263 return df
266def _makeSimpleAstropyTable(include_multidim=False, include_masked=False, include_bigendian=False):
267 """Make an astropy table for testing.
269 Parameters
270 ----------
271 include_multidim : `bool`
272 Include multi-dimensional columns.
273 include_masked : `bool`
274 Include masked columns.
275 include_bigendian : `bool`
276 Include big-endian columns.
278 Returns
279 -------
280 astropyTable : `astropy.table.Table`
281 The test table.
282 """
283 data = _makeSimpleNumpyTable(include_multidim=include_multidim, include_bigendian=include_bigendian)
284 # Add a couple of units.
285 table = atable.Table(data)
286 table["a"].unit = units.degree
287 table["a"].description = "Description of column a"
288 table["b"].unit = units.meter
289 table["b"].description = "Description of column b"
291 # Add some masked columns.
292 if include_masked:
293 nrow = len(table)
294 mask = np.zeros(nrow, dtype=bool)
295 mask[1] = True
296 # We set the masked columns with the underlying sentinel value
297 # to be able test after serialization.
299 # Masked 64-bit integer.
300 arr = np.arange(nrow, dtype="i8")
301 arr[mask] = -1
302 arr[0] = 1649900760361600113
303 table["m_i8"] = np.ma.masked_array(data=arr, mask=mask, fill_value=-1)
304 # Masked 32-bit float.
305 arr = np.arange(nrow, dtype="f4")
306 arr[mask] = np.nan
307 table["m_f4"] = np.ma.masked_array(data=arr, mask=mask, fill_value=np.nan)
308 # Unmasked 32-bit float with NaNs.
309 table["um_f4"] = arr
310 # Masked 64-bit float.
311 arr = np.arange(nrow, dtype="f8")
312 arr[mask] = np.nan
313 table["m_f8"] = np.ma.masked_array(data=arr, mask=mask, fill_value=np.nan)
314 # Unmasked 64-bit float with NaNs.
315 table["um_f8"] = arr
316 # Masked boolean.
317 arr = np.zeros(nrow, dtype=np.bool_)
318 arr[mask] = True
319 table["m_bool"] = np.ma.masked_array(data=arr, mask=mask, fill_value=True)
320 # Masked unsigned 32-bit unsigned int.
321 arr = np.arange(nrow, dtype="u4")
322 arr[mask] = 0
323 table["m_u4"] = np.ma.masked_array(data=arr, mask=mask, fill_value=0)
324 # Masked string.
325 table["m_str"] = np.ma.masked_array(data=np.array(["text"] * nrow), mask=mask, fill_value="")
326 # Masked bytes.
327 table["m_byte"] = np.ma.masked_array(data=np.array([b"bytes"] * nrow), mask=mask, fill_value=b"")
329 return table
332def _makeSimpleArrowTable(include_multidim=False, include_masked=False):
333 """Make an arrow table for testing.
335 Parameters
336 ----------
337 include_multidim : `bool`
338 Include multi-dimensional columns.
339 include_masked : `bool`
340 Include masked columns.
342 Returns
343 -------
344 arrowTable : `pyarrow.Table`
345 The test table.
346 """
347 data = _makeSimpleAstropyTable(include_multidim=include_multidim, include_masked=include_masked)
348 return astropy_to_arrow(data)
351@unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.")
352@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterDataFrame without pyarrow.")
353class ParquetFormatterDataFrameTestCase(unittest.TestCase):
354 """Tests for ParquetFormatter, DataFrame, using local file datastore."""
356 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
358 def setUp(self):
359 """Create a new butler root for each test."""
360 self.root = makeTestTempDir(TESTDIR)
361 config = Config(self.configFile)
362 self.run = "test_run"
363 self.butler = Butler.from_config(
364 Butler.makeRepo(self.root, config=config), writeable=True, run=self.run
365 )
366 self.enterContext(self.butler)
367 # No dimensions in dataset type so we don't have to worry about
368 # inserting dimension data or defining data IDs.
369 self.datasetType = DatasetType(
370 "data", dimensions=(), storageClass="DataFrame", universe=self.butler.dimensions
371 )
372 self.butler.registry.registerDatasetType(self.datasetType)
374 def tearDown(self):
375 removeTestTempDir(self.root)
377 def testSingleIndexDataFrame(self):
378 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True)
380 self.butler.put(df1, self.datasetType, dataId={})
381 # Read the whole DataFrame.
382 df2 = self.butler.get(self.datasetType, dataId={})
383 self.assertTrue(df1.equals(df2))
384 # Read just the column descriptions.
385 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
386 self.assertEqual(set(allColumns), set(columns2))
387 # Read the rowcount.
388 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
389 self.assertEqual(rowcount, len(df1))
390 # Read the schema.
391 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
392 self.assertEqual(schema, DataFrameSchema(df1))
393 # Read just some columns a few different ways.
394 df3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
395 self.assertTrue(df1.loc[:, ["a", "c"]].equals(df3))
396 df4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
397 self.assertTrue(df1.loc[:, ["a"]].equals(df4))
398 df5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
399 self.assertTrue(df1.loc[:, ["a"]].equals(df5))
400 df6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
401 self.assertTrue(df1.loc[:, ["ddd"]].equals(df6))
402 df7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
403 self.assertTrue(df1.loc[:, ["a"]].equals(df7))
404 df8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d*"]})
405 self.assertTrue(df1.loc[:, ["ddd", "dtn", "dtu"]].equals(df8))
406 df9 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d*", "d*"]})
407 self.assertTrue(df1.loc[:, ["ddd", "dtn", "dtu"]].equals(df9))
408 # Passing an unrecognized column should be a ValueError.
409 with self.assertRaises(ValueError):
410 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
412 def testSingleIndexDataFrameWithLists(self):
413 df1, allColumns = _makeSingleIndexDataFrame(include_lists=True)
415 self.butler.put(df1, self.datasetType, dataId={})
416 # Read the whole DataFrame.
417 df2 = self.butler.get(self.datasetType, dataId={})
419 # We need to check the list columns specially because they go
420 # from lists to arrays.
421 for col in ["l1", "l2", "l3"]:
422 for i in range(len(df1)):
423 self.assertTrue(np.all(df2[col].values[i] == df1[col].values[i]))
425 def testMultiIndexDataFrame(self):
426 df1 = _makeMultiIndexDataFrame()
428 self.butler.put(df1, self.datasetType, dataId={})
429 # Read the whole DataFrame.
430 df2 = self.butler.get(self.datasetType, dataId={})
431 self.assertTrue(df1.equals(df2))
432 # Read just the column descriptions.
433 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
434 self.assertTrue(df1.columns.equals(columns2))
435 self.assertEqual(columns2.names, df1.columns.names)
436 # Read the rowcount.
437 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
438 self.assertEqual(rowcount, len(df1))
439 # Read the schema.
440 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
441 self.assertEqual(schema, DataFrameSchema(df1))
442 # Read just some columns a few different ways.
443 df3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": {"filter": "g"}})
444 self.assertTrue(df1.loc[:, ["g"]].equals(df3))
445 df4 = self.butler.get(
446 self.datasetType, dataId={}, parameters={"columns": {"filter": ["r"], "column": "a"}}
447 )
448 self.assertTrue(df1.loc[:, [("r", "a")]].equals(df4))
449 column_list = [("g", "a"), ("r", "c")]
450 df5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": column_list})
451 self.assertTrue(df1.loc[:, column_list].equals(df5))
452 column_dict = {"filter": "r", "column": ["a", "b"]}
453 df6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": column_dict})
454 self.assertTrue(df1.loc[:, [("r", "a"), ("r", "b")]].equals(df6))
455 # Passing an unrecognized column should be a ValueError.
456 with self.assertRaises(ValueError):
457 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d"]})
459 def testSingleIndexDataFrameEmptyString(self):
460 """Test persisting a single index dataframe with empty strings."""
461 df1, _ = _makeSingleIndexDataFrame()
463 # Set one of the strings to None
464 df1.at[1, "strcol"] = None
466 self.butler.put(df1, self.datasetType, dataId={})
467 # Read the whole DataFrame.
468 df2 = self.butler.get(self.datasetType, dataId={})
469 self.assertTrue(df1.equals(df2))
471 def testSingleIndexDataFrameAllEmptyStrings(self):
472 """Test persisting a single index dataframe with an empty string
473 column.
474 """
475 df1, _ = _makeSingleIndexDataFrame()
477 # Set all of the strings to None
478 df1.loc[0:, "strcol"] = None
480 self.butler.put(df1, self.datasetType, dataId={})
481 # Read the whole DataFrame.
482 df2 = self.butler.get(self.datasetType, dataId={})
483 self.assertTrue(df1.equals(df2))
485 def testLegacyDataFrame(self):
486 """Test writing a dataframe to parquet via pandas (without additional
487 metadata) and ensure that we can read it back with all the new
488 functionality.
489 """
490 df1, allColumns = _makeSingleIndexDataFrame()
492 fname = os.path.join(self.root, "test_dataframe.parq")
493 df1.to_parquet(fname)
495 legacy_type = DatasetType(
496 "legacy_dataframe",
497 dimensions=(),
498 storageClass="DataFrame",
499 universe=self.butler.dimensions,
500 )
501 self.butler.registry.registerDatasetType(legacy_type)
503 data_id = {}
504 ref = DatasetRef(legacy_type, data_id, run=self.run)
505 dataset = FileDataset(path=fname, refs=[ref], formatter=ParquetFormatter)
507 self.butler.ingest(dataset, transfer="copy")
509 self.butler.put(df1, self.datasetType, dataId={})
511 df2a = self.butler.get(self.datasetType, dataId={})
512 df2b = self.butler.get("legacy_dataframe", dataId={})
513 self.assertTrue(df2a.equals(df2b))
515 df3a = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a"]})
516 df3b = self.butler.get("legacy_dataframe", dataId={}, parameters={"columns": ["a"]})
517 self.assertTrue(df3a.equals(df3b))
519 columns2a = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
520 columns2b = self.butler.get("legacy_dataframe.columns", dataId={})
521 self.assertTrue(columns2a.equals(columns2b))
523 rowcount2a = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
524 rowcount2b = self.butler.get("legacy_dataframe.rowcount", dataId={})
525 self.assertEqual(rowcount2a, rowcount2b)
527 schema2a = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
528 schema2b = self.butler.get("legacy_dataframe.schema", dataId={})
529 self.assertEqual(schema2a, schema2b)
531 def testDataFrameSchema(self):
532 tab1 = _makeSimpleArrowTable()
534 schema = DataFrameSchema.from_arrow(tab1.schema)
536 self.assertIsInstance(schema.schema, pd.DataFrame)
537 self.assertEqual(repr(schema), repr(schema._schema))
538 self.assertNotEqual(schema, "not_a_schema")
539 self.assertEqual(schema, schema)
541 tab2 = _makeMultiIndexDataFrame()
542 schema2 = DataFrameSchema(tab2)
544 self.assertNotEqual(schema, schema2)
546 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
547 def testWriteSingleIndexDataFrameReadAsAstropyTable(self):
548 df1, allColumns = _makeSingleIndexDataFrame()
550 self.butler.put(df1, self.datasetType, dataId={})
552 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
554 tab2_df = tab2.to_pandas(index="index")
555 self.assertTrue(df1.equals(tab2_df))
557 # Check reading the columns.
558 columns = list(tab2.columns.keys())
559 columns2 = self.butler.get(
560 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
561 )
562 # We check the set because pandas reorders the columns.
563 self.assertEqual(set(columns2), set(columns))
565 # Check reading the schema.
566 schema = ArrowAstropySchema(tab2)
567 schema2 = self.butler.get(
568 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema"
569 )
571 # The string types are objectified by pandas, and the order
572 # will be changed because of pandas indexing.
573 self.assertEqual(len(schema2.schema.columns), len(schema.schema.columns))
574 for name in schema.schema.columns:
575 self.assertIn(name, schema2.schema.columns)
576 if schema2.schema[name].dtype != np.dtype("O"):
577 self.assertEqual(schema2.schema[name].dtype, schema.schema[name].dtype)
579 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
580 def testWriteSingleIndexDataFrameWithMaskedColsReadAsAstropyTable(self):
581 # We need to special-case the write-as-pandas read-as-astropy code
582 # with masks because pandas has multiple ways to use masked columns.
583 # (The string column mask handling in particular is frustratingly
584 # inconsistent.)
585 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True)
587 self.butler.put(df1, self.datasetType, dataId={})
589 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
590 tab2_df = astropy_to_pandas(tab2, index="index")
592 self.assertTrue(df1.columns.equals(tab2_df.columns))
593 for name in tab2_df.columns:
594 col1 = df1[name]
595 col2 = tab2_df[name]
597 if col1.hasnans:
598 notNull = col1.notnull()
599 self.assertTrue(notNull.equals(col2.notnull()))
600 # Need to check value-by-value because column may
601 # be made of objects, depending on what pandas decides.
602 for index in notNull.values.nonzero()[0]:
603 self.assertEqual(col1[index], col2[index])
604 else:
605 self.assertTrue(col1.equals(col2))
607 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
608 def testWriteMultiIndexDataFrameReadAsAstropyTable(self):
609 df1 = _makeMultiIndexDataFrame()
611 self.butler.put(df1, self.datasetType, dataId={})
613 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
615 # This is an odd duck, it doesn't really round-trip.
616 # This test simply checks that it's readable, but definitely not
617 # recommended.
619 @unittest.skipUnless(atable is not None, "Cannot test writing as astropy without astropy.")
620 def testWriteAstropyTableWithMaskedColsReadAsSingleIndexDataFrame(self):
621 tab1 = _makeSimpleAstropyTable(include_masked=True)
623 self.butler.put(tab1, self.datasetType, dataId={})
625 tab2 = self.butler.get(self.datasetType, dataId={})
627 tab1_df = astropy_to_pandas(tab1)
628 self.assertTrue(tab1_df.equals(tab2))
630 tab2_astropy = pandas_to_astropy(tab2)
631 for col in tab1.dtype.names:
632 np.testing.assert_array_equal(tab2_astropy[col], tab1[col])
633 if isinstance(tab1[col], atable.column.MaskedColumn):
634 np.testing.assert_array_equal(tab2_astropy[col].mask, tab1[col].mask)
636 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
637 def testWriteSingleIndexDataFrameReadAsArrowTable(self):
638 df1, allColumns = _makeSingleIndexDataFrame()
640 self.butler.put(df1, self.datasetType, dataId={})
642 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
644 tab2_df = arrow_to_pandas(tab2)
645 self.assertTrue(df1.equals(tab2_df))
647 # Check reading the columns.
648 columns = list(tab2.schema.names)
649 columns2 = self.butler.get(
650 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
651 )
652 # We check the set because pandas reorders the columns.
653 self.assertEqual(set(columns), set(columns2))
655 # Override the component using a dataset type.
656 columnsType = self.datasetType.makeComponentDatasetType("columns").overrideStorageClass(
657 "ArrowColumnList"
658 )
659 self.assertEqual(columns2, self.butler.get(columnsType))
661 # Check getting a component while overriding the storage class via
662 # the dataset type. This overrides the parent storage class and then
663 # selects the component.
664 columnsType = self.datasetType.overrideStorageClass("ArrowAstropy").makeComponentDatasetType(
665 "columns"
666 )
667 self.assertEqual(columns2, self.butler.get(columnsType))
669 # Check reading the schema.
670 schema = tab2.schema
671 schema2 = self.butler.get(
672 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema"
673 )
675 # These will not have the same metadata, nor will the string column
676 # information be maintained.
677 self.assertEqual(len(schema.names), len(schema2.names))
678 for name in schema.names:
679 if schema.field(name).type not in (pa.string(), pa.binary()):
680 self.assertEqual(schema.field(name).type, schema2.field(name).type)
682 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
683 def testWriteMultiIndexDataFrameReadAsArrowTable(self):
684 df1 = _makeMultiIndexDataFrame()
686 self.butler.put(df1, self.datasetType, dataId={})
688 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
690 tab2_df = arrow_to_pandas(tab2)
691 self.assertTrue(df1.equals(tab2_df))
693 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
694 def testWriteSingleIndexDataFrameReadAsNumpyTable(self):
695 df1, allColumns = _makeSingleIndexDataFrame()
697 self.butler.put(df1, self.datasetType, dataId={})
699 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
701 tab2_df = pd.DataFrame.from_records(tab2, index=["index"])
702 self.assertTrue(df1.equals(tab2_df))
704 # Check reading the columns.
705 columns = list(tab2.dtype.names)
706 columns2 = self.butler.get(
707 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
708 )
709 # We check the set because pandas reorders the columns.
710 self.assertEqual(set(columns2), set(columns))
712 # Check reading the schema.
713 schema = ArrowNumpySchema(tab2.dtype)
714 schema2 = self.butler.get(
715 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema"
716 )
718 # The string types will be objectified by pandas, and the order
719 # will be changed because of pandas indexing.
720 self.assertEqual(len(schema.schema.names), len(schema2.schema.names))
721 for name in schema.schema.names:
722 self.assertIn(name, schema2.schema.names)
723 # It is not possible to properly track string columns via
724 # the schema consistently.
725 if schema.schema[name].type == np.dtype("O") or schema2.schema[name].type == np.dtype("O"):
726 continue
727 else:
728 self.assertEqual(schema2.schema[name].type, schema.schema[name].type)
730 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
731 def testWriteMultiIndexDataFrameReadAsNumpyTable(self):
732 df1 = _makeMultiIndexDataFrame()
734 self.butler.put(df1, self.datasetType, dataId={})
736 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
738 # This is an odd duck, it doesn't really round-trip.
739 # This test simply checks that it's readable, but definitely not
740 # recommended.
742 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.")
743 def testWriteSingleIndexDataFrameReadAsNumpyDict(self):
744 df1, allColumns = _makeSingleIndexDataFrame()
746 self.butler.put(df1, self.datasetType, dataId={})
748 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
750 tab2_df = pd.DataFrame.from_records(tab2, index=["index"])
751 # The column order is not maintained.
752 self.assertEqual(set(df1.columns), set(tab2_df.columns))
753 for col in df1.columns:
754 self.assertTrue(np.all(df1[col].values == tab2_df[col].values))
756 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.")
757 def testWriteMultiIndexDataFrameReadAsNumpyDict(self):
758 df1 = _makeMultiIndexDataFrame()
760 self.butler.put(df1, self.datasetType, dataId={})
762 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
764 # This is an odd duck, it doesn't really round-trip.
765 # This test simply checks that it's readable, but definitely not
766 # recommended.
768 def testBadDataFrameColumnParquet(self):
769 df1, allColumns = _makeSingleIndexDataFrame()
771 # Make a column with mixed type.
772 bad_col1 = [0.0] * len(df1)
773 bad_col1[1] = 0.0 * units.nJy
774 bad_df = df1.copy()
775 bad_df["bad_col1"] = bad_col1
777 # At the moment we cannot check that the correct note is added
778 # to the exception, but that will be possible in the future.
779 with self.assertRaises(RuntimeError):
780 self.butler.put(bad_df, self.datasetType, dataId={})
782 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
783 def testWriteReadAstropyTableLossless(self):
784 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
786 put_ref = self.butler.put(tab1, self.datasetType, dataId={})
788 tab2 = self.butler.get(
789 self.datasetType,
790 dataId={},
791 storageClass="ArrowAstropy",
792 parameters={"strip_astropy_meta_yaml": False},
793 )
795 # Check that minimal provenance was written by default.
796 expected = {
797 "LSST.BUTLER.ID": str(put_ref.id),
798 "LSST.BUTLER.RUN": "test_run",
799 "LSST.BUTLER.DATASETTYPE": "data",
800 "LSST.BUTLER.N_INPUTS": 0,
801 }
803 self.assertEqual(tab2.meta, expected)
805 _checkAstropyTableEquality(tab1, tab2)
807 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
808 def testWriteReadAstropyTableProvenance(self):
809 tab1 = _makeSimpleAstropyTable()
811 # Create a ref for provenance.
812 astropy_type = DatasetType(
813 "astropy_parquet",
814 dimensions=(),
815 storageClass="ArrowAstropy",
816 universe=self.butler.dimensions,
817 )
818 self.butler.registry.registerDatasetType(astropy_type)
819 input_ref = DatasetRef(astropy_type, {}, run="other_run")
820 quantum_id = uuid.uuid4()
821 provenance = DatasetProvenance(quantum_id=quantum_id)
822 provenance.add_input(input_ref)
824 put_ref = self.butler.put(tab1, self.datasetType, dataId={}, provenance=provenance)
826 tab2 = self.butler.get(
827 self.datasetType,
828 dataId={},
829 storageClass="ArrowAstropy",
830 parameters={"strip_astropy_meta_yaml": False},
831 )
833 expected = {
834 "LSST.BUTLER.ID": str(put_ref.id),
835 "LSST.BUTLER.RUN": "test_run",
836 "LSST.BUTLER.DATASETTYPE": "data",
837 "LSST.BUTLER.QUANTUM": str(quantum_id),
838 "LSST.BUTLER.N_INPUTS": 1,
839 "LSST.BUTLER.INPUT.0.ID": str(input_ref.id),
840 "LSST.BUTLER.INPUT.0.RUN": "other_run",
841 "LSST.BUTLER.INPUT.0.DATASETTYPE": "astropy_parquet",
842 }
843 self.assertEqual(tab2.meta, expected)
845 # Put the dataset again, with different provenance and ensure
846 # that the previous provenance was stripped.
847 self.butler.collections.register("new_run")
848 put_ref3 = self.butler.put(tab2, self.datasetType, dataId={}, run="new_run")
850 # tab2 will have been updated in place.
851 expected = {
852 "LSST.BUTLER.ID": str(put_ref3.id),
853 "LSST.BUTLER.RUN": "new_run",
854 "LSST.BUTLER.DATASETTYPE": "data",
855 "LSST.BUTLER.N_INPUTS": 0,
856 }
857 self.assertEqual(tab2.meta, expected)
858 null_prov, prov_ref = DatasetProvenance.from_flat_dict(tab2.meta, self.butler)
859 self.assertEqual(prov_ref, put_ref3)
860 self.assertEqual(null_prov, DatasetProvenance())
862 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
863 def testWriteReadNumpyTableLossless(self):
864 tab1 = _makeSimpleNumpyTable(include_multidim=True)
866 self.butler.put(tab1, self.datasetType, dataId={})
868 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
870 _checkNumpyTableEquality(tab1, tab2)
872 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
873 def testMaskedNumpy(self):
874 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=True)
875 tab1_np = arrow_to_numpy(tab1)
876 self.assertIsInstance(tab1_np, np.ma.MaskedArray)
877 # Stats on a masked column should ignore the nan in row 1.
878 col = tab1_np["m_f8"]
879 self.assertEqual(np.mean(col), 2.25, f"Column: {col}")
881 # Now without a mask.
882 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=False)
883 tab1_np = arrow_to_numpy(tab1)
884 self.assertNotIsInstance(tab1_np, np.ma.MaskedArray)
886 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
887 def testWriteReadArrowTableLossless(self):
888 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=True)
890 self.butler.put(tab1, self.datasetType, dataId={})
892 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
894 self.assertEqual(tab1.schema, tab2.schema)
895 tab1_np = arrow_to_numpy(tab1)
896 tab2_np = arrow_to_numpy(tab2)
897 for col in tab1.column_names:
898 np.testing.assert_array_equal(tab2_np[col], tab1_np[col])
900 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.")
901 def testWriteReadNumpyDictLossless(self):
902 tab1 = _makeSimpleNumpyTable(include_multidim=True)
903 dict1 = _numpy_to_numpy_dict(tab1)
905 self.butler.put(tab1, self.datasetType, dataId={})
907 dict2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
909 _checkNumpyDictEquality(dict1, dict2)
912@unittest.skipUnless(pd is not None, "Cannot test InMemoryDatastore with DataFrames without pandas.")
913class InMemoryDataFrameDelegateTestCase(ParquetFormatterDataFrameTestCase):
914 """Tests for InMemoryDatastore, using ArrowTableDelegate with Dataframe."""
916 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
918 def testBadDataFrameColumnParquet(self):
919 # This test does not raise for an in-memory datastore.
920 pass
922 def testWriteMultiIndexDataFrameReadAsAstropyTable(self):
923 df1 = _makeMultiIndexDataFrame()
925 self.butler.put(df1, self.datasetType, dataId={})
927 with self.assertRaises(ValueError):
928 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
930 def testLegacyDataFrame(self):
931 # This test does not work with an inMemoryDatastore.
932 pass
934 def testBadInput(self):
935 df1, _ = _makeSingleIndexDataFrame()
936 delegate = ArrowTableDelegate("DataFrame")
938 with self.assertRaises(ValueError):
939 delegate.handleParameters(inMemoryDataset="not_a_dataframe")
941 with self.assertRaises(AttributeError):
942 delegate.getComponent(composite=df1, componentName="nothing")
944 def testStorageClass(self):
945 df1, allColumns = _makeSingleIndexDataFrame()
947 factory = StorageClassFactory()
948 factory.addFromConfig(StorageClassConfig())
950 storageClass = factory.findStorageClass(type(df1), compare_types=False)
951 # Force the name lookup to do name matching.
952 storageClass._pytype = None
953 self.assertEqual(storageClass.name, "DataFrame")
955 storageClass = factory.findStorageClass(type(df1), compare_types=True)
956 # Force the name lookup to do name matching.
957 storageClass._pytype = None
958 self.assertEqual(storageClass.name, "DataFrame")
961@unittest.skipUnless(atable is not None, "Cannot test ParquetFormatterArrowAstropy without astropy.")
962@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowAstropy without pyarrow.")
963class ParquetFormatterArrowAstropyTestCase(unittest.TestCase):
964 """Tests for ParquetFormatter, ArrowAstropy, using local file datastore."""
966 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
968 def setUp(self):
969 """Create a new butler root for each test."""
970 self.root = makeTestTempDir(TESTDIR)
971 config = Config(self.configFile)
972 self.run = "test_run"
973 self.butler = Butler.from_config(
974 Butler.makeRepo(self.root, config=config), writeable=True, run=self.run
975 )
976 self.enterContext(self.butler)
977 # No dimensions in dataset type so we don't have to worry about
978 # inserting dimension data or defining data IDs.
979 self.datasetType = DatasetType(
980 "data", dimensions=(), storageClass="ArrowAstropy", universe=self.butler.dimensions
981 )
982 self.butler.registry.registerDatasetType(self.datasetType)
984 def tearDown(self):
985 removeTestTempDir(self.root)
987 def testAstropyTable(self):
988 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
990 self.butler.put(tab1, self.datasetType, dataId={})
991 # Read the whole Table.
992 tab2 = self.butler.get(self.datasetType, dataId={})
993 _checkAstropyTableEquality(tab1, tab2)
994 # Read the columns.
995 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
996 self.assertEqual(len(columns2), len(tab1.dtype.names))
997 for i, name in enumerate(tab1.dtype.names):
998 self.assertEqual(columns2[i], name)
999 # Read the rowcount.
1000 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
1001 self.assertEqual(rowcount, len(tab1))
1002 # Read the schema.
1003 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
1004 self.assertEqual(schema, ArrowAstropySchema(tab1))
1005 # Read just some columns a few different ways.
1006 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
1007 _checkAstropyTableEquality(tab1[("a", "c")], tab3)
1008 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
1009 _checkAstropyTableEquality(tab1[("a",)], tab4)
1010 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
1011 _checkAstropyTableEquality(tab1[("index", "a")], tab5)
1012 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
1013 _checkAstropyTableEquality(tab1[("ddd",)], tab6)
1014 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
1015 _checkAstropyTableEquality(tab1[("a",)], tab7)
1016 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??"]})
1017 _checkAstropyTableEquality(tab1[("ddd", "dtn", "dtu")], tab8)
1018 tab9 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]})
1019 _checkAstropyTableEquality(tab1[("ddd", "dtn", "dtu", "a")], tab9)
1020 # Passing an unrecognized column should be a ValueError.
1021 with self.assertRaises(ValueError):
1022 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
1024 def testAstropyTableBigEndian(self):
1025 tab1 = _makeSimpleAstropyTable(include_bigendian=True)
1027 self.butler.put(tab1, self.datasetType, dataId={})
1028 # Read the whole Table.
1029 tab2 = self.butler.get(self.datasetType, dataId={})
1030 _checkAstropyTableEquality(tab1, tab2, has_bigendian=True)
1032 def testAstropyTableWithMetadata(self):
1033 tab1 = _makeSimpleAstropyTable(include_multidim=True)
1035 meta = {
1036 "meta_a": 5,
1037 "meta_b": 10.0,
1038 "meta_c": [1, 2, 3],
1039 "meta_d": True,
1040 "meta_e": "string",
1041 }
1043 tab1.meta.update(meta)
1045 self.butler.put(tab1, self.datasetType, dataId={})
1046 # Read the whole Table.
1047 tab2 = self.butler.get(self.datasetType, dataId={}, parameters={"strip_astropy_meta_yaml": False})
1048 # This will check that the metadata is equivalent as well.
1049 _checkAstropyTableEquality(tab1, tab2)
1051 def testArrowAstropySchema(self):
1052 tab1 = _makeSimpleAstropyTable()
1053 tab1_arrow = astropy_to_arrow(tab1)
1054 schema = ArrowAstropySchema.from_arrow(tab1_arrow.schema)
1056 self.assertIsInstance(schema.schema, atable.Table)
1057 self.assertEqual(repr(schema), repr(schema._schema))
1058 self.assertNotEqual(schema, "not_a_schema")
1059 self.assertEqual(schema, schema)
1061 # Test various inequalities
1062 tab2 = tab1.copy()
1063 tab2.rename_column("index", "index2")
1064 schema2 = ArrowAstropySchema(tab2)
1065 self.assertNotEqual(schema2, schema)
1067 tab2 = tab1.copy()
1068 tab2["index"].unit = units.micron
1069 schema2 = ArrowAstropySchema(tab2)
1070 self.assertNotEqual(schema2, schema)
1072 tab2 = tab1.copy()
1073 tab2["index"].description = "Index column"
1074 schema2 = ArrowAstropySchema(tab2)
1075 self.assertNotEqual(schema2, schema)
1077 tab2 = tab1.copy()
1078 tab2["index"].format = "%05d"
1079 schema2 = ArrowAstropySchema(tab2)
1080 self.assertNotEqual(schema2, schema)
1082 def testAstropyParquet(self):
1083 tab1 = _makeSimpleAstropyTable()
1085 # Remove datetime column which doesn't work with astropy currently.
1086 del tab1["dtn"]
1087 del tab1["dtu"]
1089 fname = os.path.join(self.root, "test_astropy.parq")
1090 tab1.write(fname)
1092 astropy_type = DatasetType(
1093 "astropy_parquet",
1094 dimensions=(),
1095 storageClass="ArrowAstropy",
1096 universe=self.butler.dimensions,
1097 )
1098 self.butler.registry.registerDatasetType(astropy_type)
1100 data_id = {}
1101 ref = DatasetRef(astropy_type, data_id, run=self.run)
1102 dataset = FileDataset(path=fname, refs=[ref], formatter=ParquetFormatter)
1104 self.butler.ingest(dataset, transfer="copy")
1106 self.butler.put(tab1, self.datasetType, dataId={})
1108 tab2a = self.butler.get(self.datasetType, dataId={})
1109 tab2b = self.butler.get("astropy_parquet", dataId={})
1110 _checkAstropyTableEquality(tab2a, tab2b)
1112 columns2a = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1113 columns2b = self.butler.get("astropy_parquet.columns", dataId={})
1114 self.assertEqual(len(columns2b), len(columns2a))
1115 for i, name in enumerate(columns2a):
1116 self.assertEqual(columns2b[i], name)
1118 rowcount2a = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
1119 rowcount2b = self.butler.get("astropy_parquet.rowcount", dataId={})
1120 self.assertEqual(rowcount2a, rowcount2b)
1122 schema2a = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
1123 schema2b = self.butler.get("astropy_parquet.schema", dataId={})
1124 self.assertEqual(schema2a, schema2b)
1126 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
1127 def testWriteAstropyReadAsArrowTable(self):
1128 # This astropy <-> arrow works fine with masked columns.
1129 tab1 = _makeSimpleAstropyTable(include_masked=True)
1131 self.butler.put(tab1, self.datasetType, dataId={})
1133 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
1135 tab2_astropy = arrow_to_astropy(tab2)
1136 _checkAstropyTableEquality(tab1, tab2_astropy)
1138 # Check reading the columns.
1139 columns = tab2.schema.names
1140 columns2 = self.butler.get(
1141 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1142 )
1143 self.assertEqual(columns2, columns)
1145 # Check reading the schema.
1146 schema = tab2.schema
1147 schema2 = self.butler.get(
1148 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema"
1149 )
1151 self.assertEqual(schema, schema2)
1153 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1154 def testWriteAstropyReadAsDataFrame(self):
1155 tab1 = _makeSimpleAstropyTable()
1157 self.butler.put(tab1, self.datasetType, dataId={})
1159 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1161 # This is tricky because it loses the units and gains a bonus pandas
1162 # _index_ column, so we just test the dataframe form.
1164 tab1_df = tab1.to_pandas()
1165 self.assertTrue(tab1_df.equals(tab2))
1167 # Check reading the columns.
1168 columns = tab2.columns
1169 columns2 = self.butler.get(
1170 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1171 )
1172 self.assertEqual(set(columns2.to_list()), set(columns.to_list()))
1174 # Check reading the schema.
1175 schema = DataFrameSchema(tab2)
1176 schema2 = self.butler.get(
1177 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1178 )
1180 self.assertEqual(schema2, schema)
1182 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1183 def testWriteAstropyWithMaskedColsReadAsDataFrame(self):
1184 # We need to special-case the write-as-astropy read-as-pandas code
1185 # with masks because pandas has multiple ways to use masked columns.
1186 # (When writing an astropy table with masked columns we get an object
1187 # column back, but each unmasked element has the correct type.)
1188 tab1 = _makeSimpleAstropyTable(include_masked=True)
1190 self.butler.put(tab1, self.datasetType, dataId={})
1192 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1194 tab1_df = astropy_to_pandas(tab1)
1196 self.assertTrue(tab1_df.columns.equals(tab2.columns))
1197 for name in tab2.columns:
1198 col1 = tab1_df[name]
1199 col2 = tab2[name]
1201 if col1.hasnans:
1202 notNull = col1.notnull()
1203 self.assertTrue(notNull.equals(col2.notnull()))
1204 # Need to check value-by-value because column may
1205 # be made of objects, depending on what pandas decides.
1206 for index in notNull.values.nonzero()[0]:
1207 self.assertEqual(col1[index], col2[index])
1208 else:
1209 self.assertTrue(col1.equals(col2))
1211 @unittest.skipUnless(pd is not None, "Cannot test writing as a dataframe without pandas.")
1212 def testWriteSingleIndexDataFrameWithMaskedColsReadAsAstropyTable(self):
1213 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True)
1215 self.butler.put(df1, self.datasetType, dataId={})
1217 tab2 = self.butler.get(self.datasetType, dataId={})
1219 df1_tab = pandas_to_astropy(df1)
1221 _checkAstropyTableEquality(df1_tab, tab2)
1223 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1224 def testWriteAstropyReadAsNumpyTable(self):
1225 tab1 = _makeSimpleAstropyTable()
1226 self.butler.put(tab1, self.datasetType, dataId={})
1228 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
1230 # This is tricky because it loses the units.
1231 tab2_astropy = atable.Table(tab2)
1233 _checkAstropyTableEquality(tab1, tab2_astropy, skip_units=True)
1235 # Check reading the columns.
1236 columns = list(tab2.dtype.names)
1237 columns2 = self.butler.get(
1238 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1239 )
1240 self.assertEqual(columns2, columns)
1242 # Check reading the schema.
1243 schema = ArrowNumpySchema(tab2.dtype)
1244 schema2 = self.butler.get(
1245 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema"
1246 )
1248 self.assertEqual(schema2, schema)
1250 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1251 def testWriteAstropyReadAsNumpyDict(self):
1252 tab1 = _makeSimpleAstropyTable()
1253 self.butler.put(tab1, self.datasetType, dataId={})
1255 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
1257 # This is tricky because it loses the units.
1258 tab2_astropy = atable.Table(tab2)
1260 _checkAstropyTableEquality(tab1, tab2_astropy, skip_units=True)
1262 def testBadAstropyColumnParquet(self):
1263 tab1 = _makeSimpleAstropyTable()
1265 # Make a column with mixed type.
1266 bad_col1 = [0.0] * len(tab1)
1267 bad_col1[1] = 0.0 * units.nJy
1268 bad_tab = tab1.copy()
1269 bad_tab["bad_col1"] = bad_col1
1271 # At the moment we cannot check that the correct note is added
1272 # to the exception, but that will be possible in the future.
1273 with self.assertRaises(RuntimeError):
1274 self.butler.put(bad_tab, self.datasetType, dataId={})
1276 # Make a column with ragged size.
1277 bad_col2 = [[0]] * len(tab1)
1278 bad_col2[1] = [0, 0]
1279 bad_tab = tab1.copy()
1280 bad_tab["bad_col2"] = bad_col2
1282 with self.assertRaises(RuntimeError):
1283 self.butler.put(bad_tab, self.datasetType, dataId={})
1285 @unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.")
1286 def testWriteAstropyTableWithPandasIndexHint(self, testStrip=True):
1287 tab1 = _makeSimpleAstropyTable()
1289 add_pandas_index_to_astropy(tab1, "index")
1291 self.butler.put(tab1, self.datasetType, dataId={})
1293 # Read in as an astropy table and ensure index hint is still there.
1294 tab2 = self.butler.get(self.datasetType, dataId={})
1296 self.assertIn(ASTROPY_PANDAS_INDEX_KEY, tab2.meta)
1297 self.assertEqual(tab2.meta[ASTROPY_PANDAS_INDEX_KEY], "index")
1299 # Read as a dataframe and ensure index is set.
1300 df3 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1302 self.assertEqual(df3.index.name, "index")
1304 # Read as a dataframe without naming the index column.
1305 with self.assertLogs(level="WARNING") as cm:
1306 _ = self.butler.get(
1307 self.datasetType,
1308 dataId={},
1309 storageClass="DataFrame",
1310 parameters={"columns": ["a", "b"]},
1311 )
1312 self.assertIn("Index column ``index``", cm.output[0])
1314 if testStrip:
1315 # Read as an astropy table without naming the index column.
1316 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "b"]})
1318 self.assertNotIn(ASTROPY_PANDAS_INDEX_KEY, tab5.meta)
1320 with self.assertRaises(ValueError):
1321 add_pandas_index_to_astropy(tab1, "not_a_column")
1324@unittest.skipUnless(atable is not None, "Cannot test InMemoryDatastore with AstropyTable without astropy.")
1325class InMemoryArrowAstropyDelegateTestCase(ParquetFormatterArrowAstropyTestCase):
1326 """Tests for InMemoryDatastore, using ArrowTableDelegate with
1327 AstropyTable.
1328 """
1330 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
1332 def testAstropyParquet(self):
1333 # This test does not work with an inMemoryDatastore.
1334 pass
1336 def testBadAstropyColumnParquet(self):
1337 # This test does not raise for an in-memory datastore.
1338 pass
1340 def testBadInput(self):
1341 tab1 = _makeSimpleAstropyTable()
1342 delegate = ArrowTableDelegate("ArrowAstropy")
1344 with self.assertRaises(ValueError):
1345 delegate.handleParameters(inMemoryDataset="not_an_astropy_table")
1347 with self.assertRaises(NotImplementedError):
1348 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]})
1350 with self.assertRaises(AttributeError):
1351 delegate.getComponent(composite=tab1, componentName="nothing")
1353 @unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.")
1354 def testWriteAstropyTableWithPandasIndexHint(self):
1355 super().testWriteAstropyTableWithPandasIndexHint(testStrip=False)
1358@unittest.skipUnless(np is not None, "Cannot test ParquetFormatterArrowNumpy without numpy.")
1359@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowNumpy without pyarrow.")
1360class ParquetFormatterArrowNumpyTestCase(unittest.TestCase):
1361 """Tests for ParquetFormatter, ArrowNumpy, using local file datastore."""
1363 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
1365 def setUp(self):
1366 """Create a new butler root for each test."""
1367 self.root = makeTestTempDir(TESTDIR)
1368 config = Config(self.configFile)
1369 self.butler = Butler.from_config(
1370 Butler.makeRepo(self.root, config=config), writeable=True, run="test_run"
1371 )
1372 self.enterContext(self.butler)
1373 # No dimensions in dataset type so we don't have to worry about
1374 # inserting dimension data or defining data IDs.
1375 self.datasetType = DatasetType(
1376 "data", dimensions=(), storageClass="ArrowNumpy", universe=self.butler.dimensions
1377 )
1378 self.butler.registry.registerDatasetType(self.datasetType)
1380 def tearDown(self):
1381 removeTestTempDir(self.root)
1383 def testNumpyTable(self):
1384 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1386 self.butler.put(tab1, self.datasetType, dataId={})
1387 # Read the whole Table.
1388 tab2 = self.butler.get(self.datasetType, dataId={})
1389 _checkNumpyTableEquality(tab1, tab2)
1390 # Read the columns.
1391 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1392 self.assertEqual(len(columns2), len(tab1.dtype.names))
1393 for i, name in enumerate(tab1.dtype.names):
1394 self.assertEqual(columns2[i], name)
1395 # Read the rowcount.
1396 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
1397 self.assertEqual(rowcount, len(tab1))
1398 # Read the schema.
1399 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
1400 self.assertEqual(schema, ArrowNumpySchema(tab1.dtype))
1401 # Read just some columns a few different ways.
1402 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
1403 _checkNumpyTableEquality(tab1[["a", "c"]], tab3)
1404 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
1405 _checkNumpyTableEquality(
1406 tab1[
1407 [
1408 "a",
1409 ]
1410 ],
1411 tab4,
1412 )
1413 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
1414 _checkNumpyTableEquality(tab1[["index", "a"]], tab5)
1415 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
1416 _checkNumpyTableEquality(
1417 tab1[
1418 [
1419 "ddd",
1420 ]
1421 ],
1422 tab6,
1423 )
1424 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
1425 _checkNumpyTableEquality(
1426 tab1[
1427 [
1428 "a",
1429 ]
1430 ],
1431 tab7,
1432 )
1433 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]})
1434 _checkNumpyTableEquality(
1435 tab1[
1436 [
1437 "ddd",
1438 "dtn",
1439 "dtu",
1440 "a",
1441 ]
1442 ],
1443 tab8,
1444 )
1445 # Passing an unrecognized column should be a ValueError.
1446 with self.assertRaises(ValueError):
1447 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
1449 def testNumpyTableBigEndian(self):
1450 tab1 = _makeSimpleNumpyTable(include_bigendian=True)
1452 self.butler.put(tab1, self.datasetType, dataId={})
1453 # Read the whole Table.
1454 tab2 = self.butler.get(self.datasetType, dataId={})
1455 _checkNumpyTableEquality(tab1, tab2, has_bigendian=True)
1457 def testArrowNumpySchema(self):
1458 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1459 tab1_arrow = numpy_to_arrow(tab1)
1460 schema = ArrowNumpySchema.from_arrow(tab1_arrow.schema)
1462 self.assertIsInstance(schema.schema, np.dtype)
1463 self.assertEqual(repr(schema), repr(schema._dtype))
1464 self.assertNotEqual(schema, "not_a_schema")
1465 self.assertEqual(schema, schema)
1467 # Test inequality
1468 tab2 = tab1.copy()
1469 names = list(tab2.dtype.names)
1470 names[0] = "index2"
1471 tab2.dtype.names = names
1472 schema2 = ArrowNumpySchema(tab2.dtype)
1473 self.assertNotEqual(schema2, schema)
1475 @unittest.skipUnless(pa is not None, "Cannot test arrow conversions without pyarrow.")
1476 def testNumpyDictConversions(self):
1477 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1479 # Verify that everything round-trips, including the schema.
1480 tab1_arrow = numpy_to_arrow(tab1)
1481 tab1_dict = arrow_to_numpy_dict(tab1_arrow)
1482 tab1_dict_arrow = numpy_dict_to_arrow(tab1_dict)
1484 self.assertEqual(tab1_arrow.schema, tab1_dict_arrow.schema)
1485 self.assertEqual(tab1_arrow, tab1_dict_arrow)
1487 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
1488 def testWriteNumpyTableReadAsArrowTable(self):
1489 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1491 self.butler.put(tab1, self.datasetType, dataId={})
1493 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
1495 tab2_numpy = arrow_to_numpy(tab2)
1497 _checkNumpyTableEquality(tab1, tab2_numpy)
1499 # Check reading the columns.
1500 columns = tab2.schema.names
1501 columns2 = self.butler.get(
1502 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1503 )
1504 self.assertEqual(columns2, columns)
1506 # Check reading the schema.
1507 schema = tab2.schema
1508 schema2 = self.butler.get(
1509 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema"
1510 )
1511 self.assertEqual(schema2, schema)
1513 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1514 def testWriteNumpyTableReadAsDataFrame(self):
1515 tab1 = _makeSimpleNumpyTable()
1517 self.butler.put(tab1, self.datasetType, dataId={})
1519 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1521 # Converting this back to numpy gets confused with the index column
1522 # and changes the datatype of the string column.
1524 tab1_df = pd.DataFrame(tab1)
1526 self.assertTrue(tab1_df.equals(tab2))
1528 # Check reading the columns.
1529 columns = tab2.columns
1530 columns2 = self.butler.get(
1531 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1532 )
1533 self.assertEqual(set(columns2.to_list()), set(columns.to_list()))
1535 # Check reading the schema.
1536 schema = DataFrameSchema(tab2)
1537 schema2 = self.butler.get(
1538 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1539 )
1541 self.assertEqual(schema2, schema)
1543 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1544 def testWriteNumpyTableReadAsAstropyTable(self):
1545 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1547 self.butler.put(tab1, self.datasetType, dataId={})
1549 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1550 tab2_numpy = tab2.as_array()
1552 _checkNumpyTableEquality(tab1, tab2_numpy)
1554 # Check reading the columns.
1555 columns = list(tab2.columns.keys())
1556 columns2 = self.butler.get(
1557 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1558 )
1559 self.assertEqual(columns2, columns)
1561 # Check reading the schema.
1562 schema = ArrowAstropySchema(tab2)
1563 schema2 = self.butler.get(
1564 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema"
1565 )
1567 self.assertEqual(schema2, schema)
1569 def testWriteNumpyTableReadAsNumpyDict(self):
1570 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1572 self.butler.put(tab1, self.datasetType, dataId={})
1574 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
1575 tab2_numpy = _numpy_dict_to_numpy(tab2)
1577 _checkNumpyTableEquality(tab1, tab2_numpy)
1579 def testBadNumpyColumnParquet(self):
1580 tab1 = _makeSimpleAstropyTable()
1582 # Make a column with mixed type.
1583 bad_col1 = [0.0] * len(tab1)
1584 bad_col1[1] = 0.0 * units.nJy
1585 bad_tab = tab1.copy()
1586 bad_tab["bad_col1"] = bad_col1
1588 bad_tab_np = bad_tab.as_array()
1590 # At the moment we cannot check that the correct note is added
1591 # to the exception, but that will be possible in the future.
1592 with self.assertRaises(RuntimeError):
1593 self.butler.put(bad_tab_np, self.datasetType, dataId={})
1595 # Make a column with ragged size.
1596 bad_col2 = [[0]] * len(tab1)
1597 bad_col2[1] = [0, 0]
1598 bad_tab = tab1.copy()
1599 bad_tab["bad_col2"] = bad_col2
1601 bad_tab_np = bad_tab.as_array()
1603 with self.assertRaises(RuntimeError):
1604 self.butler.put(bad_tab_np, self.datasetType, dataId={})
1606 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1607 def testWriteReadAstropyTableLossless(self):
1608 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
1610 self.butler.put(tab1, self.datasetType, dataId={})
1612 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1614 _checkAstropyTableEquality(tab1, tab2)
1617@unittest.skipUnless(np is not None, "Cannot test ImMemoryDatastore with Numpy table without numpy.")
1618class InMemoryArrowNumpyDelegateTestCase(ParquetFormatterArrowNumpyTestCase):
1619 """Tests for InMemoryDatastore, using ArrowTableDelegate with
1620 Numpy table.
1621 """
1623 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
1625 def testBadNumpyColumnParquet(self):
1626 # This test does not raise for an in-memory datastore.
1627 pass
1629 def testBadInput(self):
1630 tab1 = _makeSimpleNumpyTable()
1631 delegate = ArrowTableDelegate("ArrowNumpy")
1633 with self.assertRaises(ValueError):
1634 delegate.handleParameters(inMemoryDataset="not_a_numpy_table")
1636 with self.assertRaises(NotImplementedError):
1637 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]})
1639 with self.assertRaises(AttributeError):
1640 delegate.getComponent(composite=tab1, componentName="nothing")
1642 def testStorageClass(self):
1643 tab1 = _makeSimpleNumpyTable()
1645 factory = StorageClassFactory()
1646 factory.addFromConfig(StorageClassConfig())
1648 storageClass = factory.findStorageClass(type(tab1), compare_types=False)
1649 # Force the name lookup to do name matching.
1650 storageClass._pytype = None
1651 self.assertEqual(storageClass.name, "ArrowNumpy")
1653 storageClass = factory.findStorageClass(type(tab1), compare_types=True)
1654 # Force the name lookup to do name matching.
1655 storageClass._pytype = None
1656 self.assertEqual(storageClass.name, "ArrowNumpy")
1659@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowTable without pyarrow.")
1660class ParquetFormatterArrowTableTestCase(unittest.TestCase):
1661 """Tests for ParquetFormatter, ArrowTable, using local file datastore."""
1663 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
1665 def setUp(self):
1666 """Create a new butler root for each test."""
1667 self.root = makeTestTempDir(TESTDIR)
1668 config = Config(self.configFile)
1669 self.butler = Butler.from_config(
1670 Butler.makeRepo(self.root, config=config), writeable=True, run="test_run"
1671 )
1672 self.enterContext(self.butler)
1673 # No dimensions in dataset type so we don't have to worry about
1674 # inserting dimension data or defining data IDs.
1675 self.datasetType = DatasetType(
1676 "data", dimensions=(), storageClass="ArrowTable", universe=self.butler.dimensions
1677 )
1678 self.butler.registry.registerDatasetType(self.datasetType)
1680 def tearDown(self):
1681 removeTestTempDir(self.root)
1683 def testArrowTable(self):
1684 tab1 = _makeSimpleArrowTable(include_multidim=True, include_masked=True)
1686 self.butler.put(tab1, self.datasetType, dataId={})
1687 # Read the whole Table.
1688 tab2 = self.butler.get(self.datasetType, dataId={})
1689 # We convert to use the numpy testing framework to handle nan
1690 # comparisons.
1691 self.assertEqual(tab1.schema, tab2.schema)
1692 tab1_np = arrow_to_numpy(tab1)
1693 tab2_np = arrow_to_numpy(tab2)
1694 for col in tab1.column_names:
1695 np.testing.assert_array_equal(tab2_np[col], tab1_np[col])
1696 # Read the columns.
1697 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1698 self.assertEqual(len(columns2), len(tab1.schema.names))
1699 for i, name in enumerate(tab1.schema.names):
1700 self.assertEqual(columns2[i], name)
1701 # Read the rowcount.
1702 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
1703 self.assertEqual(rowcount, len(tab1))
1704 # Read the schema.
1705 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
1706 self.assertEqual(schema, tab1.schema)
1707 # Read just some columns a few different ways.
1708 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
1709 self.assertEqual(tab3, tab1.select(("a", "c")))
1710 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
1711 self.assertEqual(tab4, tab1.select(("a",)))
1712 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
1713 self.assertEqual(tab5, tab1.select(("index", "a")))
1714 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
1715 self.assertEqual(tab6, tab1.select(("ddd",)))
1716 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
1717 self.assertEqual(tab7, tab1.select(("a",)))
1718 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a*", "d??"]})
1719 self.assertEqual(tab8, tab1.select(("a", "ddd", "dtn", "dtu")))
1720 # Passing an unrecognized column should be a ValueError.
1721 with self.assertRaises(ValueError):
1722 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
1724 def testEmptyArrowTable(self):
1725 data = _makeSimpleNumpyTable()
1726 type_list = _numpy_dtype_to_arrow_types(data.dtype)
1728 schema = pa.schema(type_list)
1729 arrays = [[]] * len(schema.names)
1731 tab1 = pa.Table.from_arrays(arrays, schema=schema)
1733 self.butler.put(tab1, self.datasetType, dataId={})
1734 tab2 = self.butler.get(self.datasetType, dataId={})
1735 self.assertEqual(tab2, tab1)
1737 tab1_numpy = arrow_to_numpy(tab1)
1738 self.assertEqual(len(tab1_numpy), 0)
1739 tab1_numpy_arrow = numpy_to_arrow(tab1_numpy)
1740 self.assertEqual(tab1_numpy_arrow, tab1)
1742 tab1_pandas = arrow_to_pandas(tab1)
1743 self.assertEqual(len(tab1_pandas), 0)
1744 tab1_pandas_arrow = pandas_to_arrow(tab1_pandas)
1745 # Unfortunately, string/byte columns get mangled when translated
1746 # through empty pandas dataframes.
1747 self.assertEqual(
1748 tab1_pandas_arrow.select(("index", "a", "b", "c", "ddd")),
1749 tab1.select(("index", "a", "b", "c", "ddd")),
1750 )
1752 tab1_astropy = arrow_to_astropy(tab1)
1753 self.assertEqual(len(tab1_astropy), 0)
1754 tab1_astropy_arrow = astropy_to_arrow(tab1_astropy)
1755 self.assertEqual(tab1_astropy_arrow, tab1)
1757 def testEmptyArrowTableMultidim(self):
1758 data = _makeSimpleNumpyTable(include_multidim=True)
1759 type_list = _numpy_dtype_to_arrow_types(data.dtype)
1761 md = {}
1762 for name in data.dtype.names:
1763 _append_numpy_multidim_metadata(md, name, data.dtype[name])
1765 schema = pa.schema(type_list, metadata=md)
1766 arrays = [[]] * len(schema.names)
1768 tab1 = pa.Table.from_arrays(arrays, schema=schema)
1770 self.butler.put(tab1, self.datasetType, dataId={})
1771 tab2 = self.butler.get(self.datasetType, dataId={})
1772 self.assertEqual(tab2, tab1)
1774 tab1_numpy = arrow_to_numpy(tab1)
1775 self.assertEqual(len(tab1_numpy), 0)
1776 tab1_numpy_arrow = numpy_to_arrow(tab1_numpy)
1777 self.assertEqual(tab1_numpy_arrow, tab1)
1779 tab1_astropy = arrow_to_astropy(tab1)
1780 self.assertEqual(len(tab1_astropy), 0)
1781 tab1_astropy_arrow = astropy_to_arrow(tab1_astropy)
1782 self.assertEqual(tab1_astropy_arrow, tab1)
1784 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1785 def testWriteArrowTableReadAsSingleIndexDataFrame(self):
1786 df1, allColumns = _makeSingleIndexDataFrame()
1788 self.butler.put(df1, self.datasetType, dataId={})
1790 # Read back out as a dataframe.
1791 df2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1792 self.assertTrue(df1.equals(df2))
1794 # Read back out as an arrow table, convert to dataframe.
1795 tab3 = self.butler.get(self.datasetType, dataId={})
1796 df3 = arrow_to_pandas(tab3)
1797 self.assertTrue(df1.equals(df3))
1799 # Check reading the columns.
1800 columns = df2.reset_index().columns
1801 columns2 = self.butler.get(
1802 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1803 )
1804 # We check the set because pandas reorders the columns.
1805 self.assertEqual(set(columns2.to_list()), set(columns.to_list()))
1807 # Check reading the schema.
1808 schema = DataFrameSchema(df1)
1809 schema2 = self.butler.get(
1810 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1811 )
1812 self.assertEqual(schema2, schema)
1814 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1815 def testWriteArrowTableReadAsMultiIndexDataFrame(self):
1816 df1 = _makeMultiIndexDataFrame()
1818 self.butler.put(df1, self.datasetType, dataId={})
1820 # Read back out as a dataframe.
1821 df2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1822 self.assertTrue(df1.equals(df2))
1824 # Read back out as an arrow table, convert to dataframe.
1825 atab3 = self.butler.get(self.datasetType, dataId={})
1826 df3 = arrow_to_pandas(atab3)
1827 self.assertTrue(df1.equals(df3))
1829 # Check reading the columns.
1830 columns = df2.columns
1831 columns2 = self.butler.get(
1832 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1833 )
1834 self.assertTrue(columns2.equals(columns))
1836 # Check reading the schema.
1837 schema = DataFrameSchema(df1)
1838 schema2 = self.butler.get(
1839 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1840 )
1841 self.assertEqual(schema2, schema)
1843 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1844 def testWriteArrowTableReadAsAstropyTable(self):
1845 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
1847 self.butler.put(tab1, self.datasetType, dataId={})
1849 # Read back out as an astropy table.
1850 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1851 _checkAstropyTableEquality(tab1, tab2)
1853 # Read back out as an arrow table, convert to astropy table.
1854 atab3 = self.butler.get(self.datasetType, dataId={})
1855 tab3 = arrow_to_astropy(atab3)
1856 _checkAstropyTableEquality(tab1, tab3)
1858 # Check reading the columns.
1859 columns = list(tab2.columns.keys())
1860 columns2 = self.butler.get(
1861 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1862 )
1863 self.assertEqual(columns2, columns)
1865 # Check reading the schema.
1866 schema = ArrowAstropySchema(tab1)
1867 schema2 = self.butler.get(
1868 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema"
1869 )
1870 self.assertEqual(schema2, schema)
1872 # Check the schema conversions and units.
1873 arrow_schema = schema.to_arrow_schema()
1874 for name in arrow_schema.names:
1875 field_metadata = arrow_schema.field(name).metadata
1876 if (
1877 b"description" in field_metadata
1878 and (description := field_metadata[b"description"].decode("UTF-8")) != ""
1879 ):
1880 self.assertEqual(schema2.schema[name].description, description)
1881 else:
1882 self.assertIsNone(schema2.schema[name].description)
1883 if b"unit" in field_metadata and (unit := field_metadata[b"unit"].decode("UTF-8")) != "":
1884 self.assertEqual(schema2.schema[name].unit, units.Unit(unit))
1886 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1887 def testWriteArrowTableReadAsNumpyTable(self):
1888 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1890 self.butler.put(tab1, self.datasetType, dataId={})
1892 # Read back out as a numpy table.
1893 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
1894 _checkNumpyTableEquality(tab1, tab2)
1896 # Read back out as an arrow table, convert to numpy table.
1897 atab3 = self.butler.get(self.datasetType, dataId={})
1898 tab3 = arrow_to_numpy(atab3)
1899 _checkNumpyTableEquality(tab1, tab3)
1901 # Check reading the columns.
1902 columns = list(tab2.dtype.names)
1903 columns2 = self.butler.get(
1904 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1905 )
1906 self.assertEqual(columns2, columns)
1908 # Check reading the schema.
1909 schema = ArrowNumpySchema(tab1.dtype)
1910 schema2 = self.butler.get(
1911 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema"
1912 )
1913 self.assertEqual(schema2, schema)
1915 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1916 def testWriteArrowTableReadAsNumpyDict(self):
1917 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1919 self.butler.put(tab1, self.datasetType, dataId={})
1921 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
1922 tab2_numpy = _numpy_dict_to_numpy(tab2)
1923 _checkNumpyTableEquality(tab1, tab2_numpy)
1925 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1926 def testWriteReadAstropyTableLossless(self):
1927 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
1929 self.butler.put(tab1, self.datasetType, dataId={})
1931 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1933 _checkAstropyTableEquality(tab1, tab2)
1936@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with ArroWTable without pyarrow.")
1937class InMemoryArrowTableDelegateTestCase(ParquetFormatterArrowTableTestCase):
1938 """Tests for InMemoryDatastore, using ArrowTableDelegate."""
1940 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
1942 def testBadInput(self):
1943 tab1 = _makeSimpleArrowTable()
1944 delegate = ArrowTableDelegate("ArrowTable")
1946 with self.assertRaises(ValueError):
1947 delegate.handleParameters(inMemoryDataset="not_an_arrow_table")
1949 with self.assertRaises(NotImplementedError):
1950 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]})
1952 with self.assertRaises(AttributeError):
1953 delegate.getComponent(composite=tab1, componentName="nothing")
1955 def testStorageClass(self):
1956 tab1 = _makeSimpleArrowTable()
1958 factory = StorageClassFactory()
1959 factory.addFromConfig(StorageClassConfig())
1961 storageClass = factory.findStorageClass(type(tab1), compare_types=False)
1962 # Force the name lookup to do name matching.
1963 storageClass._pytype = None
1964 self.assertEqual(storageClass.name, "ArrowTable")
1966 storageClass = factory.findStorageClass(type(tab1), compare_types=True)
1967 # Force the name lookup to do name matching.
1968 storageClass._pytype = None
1969 self.assertEqual(storageClass.name, "ArrowTable")
1972@unittest.skipUnless(np is not None, "Cannot test ParquetFormatterArrowNumpy without numpy.")
1973@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowNumpy without pyarrow.")
1974class ParquetFormatterArrowNumpyDictTestCase(unittest.TestCase):
1975 """Tests for ParquetFormatter, ArrowNumpyDict, using local file store."""
1977 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
1979 def setUp(self):
1980 """Create a new butler root for each test."""
1981 self.root = makeTestTempDir(TESTDIR)
1982 config = Config(self.configFile)
1983 self.butler = Butler.from_config(
1984 Butler.makeRepo(self.root, config=config), writeable=True, run="test_run"
1985 )
1986 self.enterContext(self.butler)
1987 # No dimensions in dataset type so we don't have to worry about
1988 # inserting dimension data or defining data IDs.
1989 self.datasetType = DatasetType(
1990 "data", dimensions=(), storageClass="ArrowNumpyDict", universe=self.butler.dimensions
1991 )
1992 self.butler.registry.registerDatasetType(self.datasetType)
1994 def tearDown(self):
1995 removeTestTempDir(self.root)
1997 def testNumpyDict(self):
1998 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1999 dict1 = _numpy_to_numpy_dict(tab1)
2001 self.butler.put(dict1, self.datasetType, dataId={})
2002 # Read the whole table.
2003 dict2 = self.butler.get(self.datasetType, dataId={})
2004 _checkNumpyDictEquality(dict1, dict2)
2005 # Read the columns.
2006 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
2007 self.assertEqual(len(columns2), len(dict1.keys()))
2008 for name in dict1:
2009 self.assertIn(name, columns2)
2010 # Read the rowcount.
2011 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
2012 self.assertEqual(rowcount, len(dict1["a"]))
2013 # Read the schema.
2014 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
2015 self.assertEqual(schema, ArrowNumpySchema(tab1.dtype))
2016 # Read just some columns a few different ways.
2017 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
2018 subdict = {key: dict1[key] for key in ["a", "c"]}
2019 _checkNumpyDictEquality(subdict, tab3)
2020 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
2021 subdict = {key: dict1[key] for key in ["a"]}
2022 _checkNumpyDictEquality(subdict, tab4)
2023 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
2024 subdict = {key: dict1[key] for key in ["index", "a"]}
2025 _checkNumpyDictEquality(subdict, tab5)
2026 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
2027 subdict = {key: dict1[key] for key in ["ddd"]}
2028 _checkNumpyDictEquality(subdict, tab6)
2029 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
2030 subdict = {key: dict1[key] for key in ["a"]}
2031 _checkNumpyDictEquality(subdict, tab7)
2032 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]})
2033 subdict = {key: dict1[key] for key in ["ddd", "dtn", "dtu", "a"]}
2034 _checkNumpyDictEquality(subdict, tab8)
2035 # Passing an unrecognized column should be a ValueError.
2036 with self.assertRaises(ValueError):
2037 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
2039 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
2040 def testWriteNumpyDictReadAsArrowTable(self):
2041 tab1 = _makeSimpleNumpyTable(include_multidim=True)
2042 dict1 = _numpy_to_numpy_dict(tab1)
2044 self.butler.put(dict1, self.datasetType, dataId={})
2046 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
2048 tab2_dict = arrow_to_numpy_dict(tab2)
2050 _checkNumpyDictEquality(dict1, tab2_dict)
2052 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
2053 def testWriteNumpyDictReadAsDataFrame(self):
2054 tab1 = _makeSimpleNumpyTable()
2055 dict1 = _numpy_to_numpy_dict(tab1)
2057 self.butler.put(dict1, self.datasetType, dataId={})
2059 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
2061 # The order of the dict may get mixed up, so we need to check column
2062 # by column. We also need to do this in dataframe form because pandas
2063 # changes the datatype of the string column.
2064 tab1_df = pd.DataFrame(tab1)
2066 self.assertEqual(set(tab1_df.columns), set(tab2.columns))
2067 for col in tab1_df.columns:
2068 self.assertTrue(np.all(tab1_df[col].values == tab2[col].values))
2070 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
2071 def testWriteNumpyDictReadAsAstropyTable(self):
2072 tab1 = _makeSimpleNumpyTable(include_multidim=True)
2073 dict1 = _numpy_to_numpy_dict(tab1)
2075 self.butler.put(dict1, self.datasetType, dataId={})
2077 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
2078 tab2_dict = _astropy_to_numpy_dict(tab2)
2080 _checkNumpyDictEquality(dict1, tab2_dict)
2082 def testWriteNumpyDictReadAsNumpyTable(self):
2083 tab1 = _makeSimpleNumpyTable(include_multidim=True)
2084 dict1 = _numpy_to_numpy_dict(tab1)
2086 self.butler.put(dict1, self.datasetType, dataId={})
2088 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
2089 tab2_dict = _numpy_to_numpy_dict(tab2)
2091 _checkNumpyDictEquality(dict1, tab2_dict)
2093 def testWriteNumpyDictBad(self):
2094 dict1 = {"a": 4, "b": np.ndarray([1])}
2095 with self.assertRaises(RuntimeError):
2096 self.butler.put(dict1, self.datasetType, dataId={})
2098 dict2 = {"a": np.zeros(4), "b": np.zeros(5)}
2099 with self.assertRaises(RuntimeError):
2100 self.butler.put(dict2, self.datasetType, dataId={})
2102 dict3 = {"a": [0] * 5, "b": np.zeros(5)}
2103 with self.assertRaises(RuntimeError):
2104 self.butler.put(dict3, self.datasetType, dataId={})
2106 dict4 = {"a": np.zeros(4), "b": np.zeros(4, dtype="O")}
2107 with self.assertRaises(RuntimeError):
2108 self.butler.put(dict4, self.datasetType, dataId={})
2110 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
2111 def testWriteReadAstropyTableLossless(self):
2112 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
2114 self.butler.put(tab1, self.datasetType, dataId={})
2116 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
2118 _checkAstropyTableEquality(tab1, tab2)
2121@unittest.skipUnless(np is not None, "Cannot test InMemoryDatastore with NumpyDict without numpy.")
2122@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with NumpyDict without pyarrow.")
2123class InMemoryNumpyDictDelegateTestCase(ParquetFormatterArrowNumpyDictTestCase):
2124 """Tests for InMemoryDatastore, using ArrowTableDelegate with
2125 Numpy dict.
2126 """
2128 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
2130 def testWriteNumpyDictBad(self):
2131 # The sub-type checking is not done on in-memory datastore.
2132 pass
2135@unittest.skipUnless(pa is not None, "Cannot test ArrowSchema without pyarrow.")
2136class ParquetFormatterArrowSchemaTestCase(unittest.TestCase):
2137 """Tests for ParquetFormatter, ArrowSchema, using local file datastore."""
2139 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
2141 def setUp(self):
2142 """Create a new butler root for each test."""
2143 self.root = makeTestTempDir(TESTDIR)
2144 config = Config(self.configFile)
2145 self.butler = Butler.from_config(
2146 Butler.makeRepo(self.root, config=config), writeable=True, run="test_run"
2147 )
2148 self.enterContext(self.butler)
2149 # No dimensions in dataset type so we don't have to worry about
2150 # inserting dimension data or defining data IDs.
2151 self.datasetType = DatasetType(
2152 "data", dimensions=(), storageClass="ArrowSchema", universe=self.butler.dimensions
2153 )
2154 self.butler.registry.registerDatasetType(self.datasetType)
2156 def tearDown(self):
2157 removeTestTempDir(self.root)
2159 def _makeTestSchema(self):
2160 schema = pa.schema(
2161 [
2162 pa.field(
2163 "int32",
2164 pa.int32(),
2165 nullable=False,
2166 metadata={
2167 "description": "32-bit integer",
2168 "unit": "",
2169 },
2170 ),
2171 pa.field(
2172 "int64",
2173 pa.int64(),
2174 nullable=False,
2175 metadata={
2176 "description": "64-bit integer",
2177 "unit": "",
2178 },
2179 ),
2180 pa.field(
2181 "uint64",
2182 pa.uint64(),
2183 nullable=False,
2184 metadata={
2185 "description": "64-bit unsigned integer",
2186 "unit": "",
2187 },
2188 ),
2189 pa.field(
2190 "float32",
2191 pa.float32(),
2192 nullable=False,
2193 metadata={
2194 "description": "32-bit float",
2195 "unit": "count",
2196 },
2197 ),
2198 pa.field(
2199 "float64",
2200 pa.float64(),
2201 nullable=False,
2202 metadata={
2203 "description": "64-bit float",
2204 "unit": "nJy",
2205 },
2206 ),
2207 pa.field(
2208 "fixed_size_list",
2209 pa.list_(pa.float64(), list_size=10),
2210 nullable=False,
2211 metadata={
2212 "description": "Fixed size list of 64-bit floats.",
2213 "unit": "nJy",
2214 },
2215 ),
2216 pa.field(
2217 "variable_size_list",
2218 pa.list_(pa.float64()),
2219 nullable=False,
2220 metadata={
2221 "description": "Variable size list of 64-bit floats.",
2222 "unit": "nJy",
2223 },
2224 ),
2225 # One of these fields will have no description.
2226 pa.field(
2227 "string",
2228 pa.string(),
2229 nullable=False,
2230 metadata={
2231 "unit": "",
2232 },
2233 ),
2234 # One of these fields will have no metadata.
2235 pa.field(
2236 "binary",
2237 pa.binary(),
2238 nullable=False,
2239 ),
2240 ]
2241 )
2243 return schema
2245 def testArrowSchema(self):
2246 schema1 = self._makeTestSchema()
2247 self.butler.put(schema1, self.datasetType, dataId={})
2249 schema2 = self.butler.get(self.datasetType, dataId={})
2250 self.assertEqual(schema2, schema1)
2252 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe schema without pandas.")
2253 def testWriteArrowSchemaReadAsDataFrameSchema(self):
2254 schema1 = self._makeTestSchema()
2255 self.butler.put(schema1, self.datasetType, dataId={})
2257 df_schema1 = DataFrameSchema.from_arrow(schema1)
2259 df_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrameSchema")
2260 self.assertEqual(df_schema2, df_schema1)
2262 @unittest.skipUnless(atable is not None, "Cannot test reading as an astropy schema without astropy.")
2263 def testWriteArrowSchemaReadAsArrowAstropySchema(self):
2264 schema1 = self._makeTestSchema()
2265 self.butler.put(schema1, self.datasetType, dataId={})
2267 ap_schema1 = ArrowAstropySchema.from_arrow(schema1)
2269 ap_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropySchema")
2270 self.assertEqual(ap_schema2, ap_schema1)
2272 # Confirm that the ap_schema2 has the unit/description we expect.
2273 for name in schema1.names:
2274 field_metadata = schema1.field(name).metadata
2275 if field_metadata is None:
2276 continue
2277 if (
2278 b"description" in field_metadata
2279 and (description := field_metadata[b"description"].decode("UTF-8")) != ""
2280 ):
2281 self.assertEqual(ap_schema2.schema[name].description, description)
2282 else:
2283 self.assertIsNone(ap_schema2.schema[name].description)
2284 if b"unit" in field_metadata and (unit := field_metadata[b"unit"].decode("UTF-8")) != "":
2285 self.assertEqual(ap_schema2.schema[name].unit, units.Unit(unit))
2287 @unittest.skipUnless(atable is not None, "Cannot test reading as an numpy schema without numpy.")
2288 def testWriteArrowSchemaReadAsArrowNumpySchema(self):
2289 schema1 = self._makeTestSchema()
2290 self.butler.put(schema1, self.datasetType, dataId={})
2292 np_schema1 = ArrowNumpySchema.from_arrow(schema1)
2294 np_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpySchema")
2295 self.assertEqual(np_schema2, np_schema1)
2298@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with ArrowSchema without pyarrow.")
2299class InMemoryArrowSchemaDelegateTestCase(ParquetFormatterArrowSchemaTestCase):
2300 """Tests for InMemoryDatastore and ArrowSchema."""
2302 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
2305@unittest.skipUnless(pa is not None, "Cannot test S3 without pyarrow.")
2306@unittest.skipUnless(boto3 is not None, "Cannot test S3 without boto3.")
2307@unittest.skipUnless(fsspec is not None, "Cannot test S3 without fsspec.")
2308@unittest.skipUnless(s3fs is not None, "Cannot test S3 without s3fs.")
2309class ParquetFormatterArrowTableS3TestCase(unittest.TestCase):
2310 """Tests for arrow table/parquet with S3."""
2312 # Code is adapted from test_butler.py
2313 configFile = os.path.join(TESTDIR, "config/basic/butler-s3store.yaml")
2314 fullConfigKey = None
2315 validationCanFail = True
2317 bucketName = "anybucketname"
2319 root = "butlerRoot/"
2321 datastoreStr = [f"datastore={root}"]
2323 datastoreName = ["FileDatastore@s3://{bucketName}/{root}"]
2325 registryStr = "/gen3.sqlite3"
2327 mock_aws = mock_aws()
2329 def setUp(self):
2330 self.root = makeTestTempDir(TESTDIR)
2332 config = Config(self.configFile)
2333 uri = ResourcePath(config[".datastore.datastore.root"])
2334 self.bucketName = uri.netloc
2336 # Enable S3 mocking of tests.
2337 self.enterContext(clean_test_environment_for_s3())
2338 self.mock_aws.start()
2340 rooturi = f"s3://{self.bucketName}/{self.root}"
2341 config.update({"datastore": {"datastore": {"root": rooturi}}})
2343 # need local folder to store registry database
2344 self.reg_dir = makeTestTempDir(TESTDIR)
2345 config["registry", "db"] = f"sqlite:///{self.reg_dir}/gen3.sqlite3"
2347 # MOTO needs to know that we expect Bucket bucketname to exist
2348 # (this used to be the class attribute bucketName)
2349 s3 = boto3.resource("s3")
2350 s3.create_bucket(Bucket=self.bucketName)
2352 self.datastoreStr = [f"datastore='{rooturi}'"]
2353 self.datastoreName = [f"FileDatastore@{rooturi}"]
2354 Butler.makeRepo(rooturi, config=config, forceConfigRoot=False)
2355 self.tmpConfigFile = posixpath.join(rooturi, "butler.yaml")
2357 self.butler = Butler(self.tmpConfigFile, writeable=True, run="test_run")
2358 self.enterContext(self.butler)
2360 # No dimensions in dataset type so we don't have to worry about
2361 # inserting dimension data or defining data IDs.
2362 self.datasetType = DatasetType(
2363 "data", dimensions=(), storageClass="ArrowTable", universe=self.butler.dimensions
2364 )
2365 self.butler.registry.registerDatasetType(self.datasetType)
2367 def tearDown(self):
2368 s3 = boto3.resource("s3")
2369 bucket = s3.Bucket(self.bucketName)
2370 try:
2371 bucket.objects.all().delete()
2372 except botocore.exceptions.ClientError as e:
2373 if e.response["Error"]["Code"] == "404":
2374 # the key was not reachable - pass
2375 pass
2376 else:
2377 raise
2379 bucket = s3.Bucket(self.bucketName)
2380 bucket.delete()
2382 # Stop the S3 mock.
2383 self.mock_aws.stop()
2385 if self.reg_dir is not None and os.path.exists(self.reg_dir): 2385 ↛ 2388line 2385 didn't jump to line 2388 because the condition on line 2385 was always true
2386 shutil.rmtree(self.reg_dir, ignore_errors=True)
2388 if os.path.exists(self.root): 2388 ↛ exitline 2388 didn't return from function 'tearDown' because the condition on line 2388 was always true
2389 shutil.rmtree(self.root, ignore_errors=True)
2391 def testArrowTableS3(self):
2392 tab1 = _makeSimpleArrowTable(include_multidim=True, include_masked=True)
2394 self.butler.put(tab1, self.datasetType, dataId={})
2396 # Read the whole Table.
2397 tab2 = self.butler.get(self.datasetType, dataId={})
2398 # We convert to use the numpy testing framework to handle nan
2399 # comparisons.
2400 self.assertEqual(tab1.schema, tab2.schema)
2401 tab1_np = arrow_to_numpy(tab1)
2402 tab2_np = arrow_to_numpy(tab2)
2403 for col in tab1.column_names:
2404 np.testing.assert_array_equal(tab2_np[col], tab1_np[col])
2405 # Read the columns.
2406 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
2407 self.assertEqual(len(columns2), len(tab1.schema.names))
2408 for i, name in enumerate(tab1.schema.names):
2409 self.assertEqual(columns2[i], name)
2410 # Read the rowcount.
2411 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
2412 self.assertEqual(rowcount, len(tab1))
2413 # Read the schema.
2414 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
2415 self.assertEqual(schema, tab1.schema)
2416 # Read just some columns a few different ways.
2417 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
2418 self.assertEqual(tab3, tab1.select(("a", "c")))
2419 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
2420 self.assertEqual(tab4, tab1.select(("a",)))
2421 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
2422 self.assertEqual(tab5, tab1.select(("index", "a")))
2423 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
2424 self.assertEqual(tab6, tab1.select(("ddd",)))
2425 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
2426 self.assertEqual(tab7, tab1.select(("a",)))
2427 # Passing an unrecognized column should be a ValueError.
2428 with self.assertRaises(ValueError):
2429 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
2432@unittest.skipUnless(np is not None, "Cannot test compute_row_group_size without numpy.")
2433@unittest.skipUnless(pa is not None, "Cannot test compute_row_group_size without pyarrow.")
2434class ComputeRowGroupSizeTestCase(unittest.TestCase):
2435 """Tests for compute_row_group_size."""
2437 def testRowGroupSizeNoMetadata(self):
2438 numpyTable = _makeSimpleNumpyTable(include_multidim=True)
2440 # We can't use the numpy_to_arrow convenience function because
2441 # that adds metadata.
2442 type_list = _numpy_dtype_to_arrow_types(numpyTable.dtype)
2443 schema = pa.schema(type_list)
2444 arrays = _numpy_style_arrays_to_arrow_arrays(
2445 numpyTable.dtype,
2446 len(numpyTable),
2447 numpyTable,
2448 schema,
2449 )
2450 arrowTable = pa.Table.from_arrays(arrays, schema=schema)
2452 row_group_size = compute_row_group_size(arrowTable.schema)
2454 self.assertGreater(row_group_size, 1_000_000)
2455 self.assertLess(row_group_size, 2_000_000)
2457 def testRowGroupSizeWithMetadata(self):
2458 numpyTable = _makeSimpleNumpyTable(include_multidim=True)
2460 arrowTable = numpy_to_arrow(numpyTable)
2462 row_group_size = compute_row_group_size(arrowTable.schema)
2464 self.assertGreater(row_group_size, 1_000_000)
2465 self.assertLess(row_group_size, 2_000_000)
2467 def testRowGroupSizeTinyTable(self):
2468 numpyTable = np.zeros(1, dtype=[("a", np.bool_)])
2470 arrowTable = numpy_to_arrow(numpyTable)
2472 row_group_size = compute_row_group_size(arrowTable.schema)
2474 self.assertGreater(row_group_size, 1_000_000)
2476 @unittest.skipUnless(pd is not None, "Cannot run testRowGroupSizeDataFrameWithLists without pandas.")
2477 def testRowGroupSizeDataFrameWithLists(self):
2478 df = pd.DataFrame({"a": np.zeros(10), "b": [[0, 0]] * 10, "c": [[0.0, 0.0]] * 10, "d": [[]] * 10})
2479 arrowTable = pandas_to_arrow(df)
2480 row_group_size = compute_row_group_size(arrowTable.schema)
2482 self.assertGreater(row_group_size, 1_000_000)
2485def _checkAstropyTableEquality(table1, table2, skip_units=False, has_bigendian=False):
2486 """Check if two astropy tables have the same columns/values.
2488 Parameters
2489 ----------
2490 table1 : `astropy.table.Table`
2491 table2 : `astropy.table.Table`
2492 skip_units : `bool`
2493 has_bigendian : `bool`
2494 """
2495 if not has_bigendian:
2496 assert table1.dtype == table2.dtype
2497 else:
2498 for name in table1.dtype.names:
2499 # Only check type matches, force to little-endian.
2500 assert table1.dtype[name].newbyteorder(">") == table2.dtype[name].newbyteorder(">")
2502 # Strip provenance before comparison.
2503 DatasetProvenance.strip_provenance_from_flat_dict(table1.meta)
2504 DatasetProvenance.strip_provenance_from_flat_dict(table2.meta)
2505 assert table1.meta == table2.meta
2506 if not skip_units:
2507 for name in table1.columns:
2508 assert table1[name].unit == table2[name].unit
2509 assert table1[name].description == table2[name].description
2510 assert table1[name].format == table2[name].format
2512 for name in table1.columns:
2513 # We need to check masked/regular columns after filling.
2514 has_masked = False
2515 if isinstance(table1[name], atable.column.MaskedColumn):
2516 c1 = table1[name].filled()
2517 has_masked = True
2518 else:
2519 c1 = np.array(table1[name])
2520 if has_masked:
2521 assert isinstance(table2[name], atable.column.MaskedColumn)
2522 c2 = table2[name].filled()
2523 else:
2524 assert not isinstance(table2[name], atable.column.MaskedColumn)
2525 c2 = np.array(table2[name])
2526 np.testing.assert_array_equal(c1, c2)
2527 # If we have a masked column then we test the underlying data.
2528 if has_masked:
2529 np.testing.assert_array_equal(np.array(c1), np.array(c2))
2530 np.testing.assert_array_equal(table1[name].mask, table2[name].mask)
2533def _checkNumpyTableEquality(table1, table2, has_bigendian=False):
2534 """Check if two numpy tables have the same columns/values
2536 Parameters
2537 ----------
2538 table1 : `numpy.ndarray`
2539 table2 : `numpy.ndarray`
2540 has_bigendian : `bool`
2541 """
2542 assert table1.dtype.names == table2.dtype.names
2543 for name in table1.dtype.names:
2544 if not has_bigendian:
2545 assert table1.dtype[name] == table2.dtype[name]
2546 else:
2547 # Only check type matches, force to little-endian.
2548 assert table1.dtype[name].newbyteorder(">") == table2.dtype[name].newbyteorder(">")
2549 assert np.all(table1 == table2)
2552def _checkNumpyDictEquality(dict1, dict2):
2553 """Check if two numpy dicts have the same columns/values.
2555 Parameters
2556 ----------
2557 dict1 : `dict` [`str`, `np.ndarray`]
2558 dict2 : `dict` [`str`, `np.ndarray`]
2559 """
2560 assert set(dict1.keys()) == set(dict2.keys())
2561 for name in dict1:
2562 assert dict1[name].dtype == dict2[name].dtype
2563 assert np.all(dict1[name] == dict2[name])
2566if __name__ == "__main__":
2567 unittest.main()