Coverage for tests/test_parquet.py: 99%
1307 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 15:00 -0700
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 15:00 -0700
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 unittest
36import uuid
38try:
39 import pyarrow as pa
40except ImportError:
41 pa = None
42try:
43 import astropy.table as atable
44 from astropy import units
45except ImportError:
46 atable = None
47try:
48 import numpy as np
49except ImportError:
50 np = None
51try:
52 import pandas as pd
53except ImportError:
54 pd = None
56try:
57 import fsspec
58except ImportError:
59 fsspec = None
62from lsst.daf.butler import (
63 Butler,
64 Config,
65 DatasetProvenance,
66 DatasetRef,
67 DatasetType,
68 FileDataset,
69 StorageClassConfig,
70 StorageClassFactory,
71)
73try:
74 from lsst.daf.butler.delegates.arrowtable import ArrowTableDelegate
75except ImportError:
76 pa = None
78try:
79 from lsst.daf.butler.formatters.parquet import (
80 ASTROPY_PANDAS_INDEX_KEY,
81 ArrowAstropySchema,
82 ArrowNumpySchema,
83 DataFrameSchema,
84 ParquetFormatter,
85 _append_numpy_multidim_metadata,
86 _astropy_to_numpy_dict,
87 _numpy_dict_to_numpy,
88 _numpy_dtype_to_arrow_types,
89 _numpy_style_arrays_to_arrow_arrays,
90 _numpy_to_numpy_dict,
91 add_pandas_index_to_astropy,
92 arrow_to_astropy,
93 arrow_to_numpy,
94 arrow_to_numpy_dict,
95 arrow_to_pandas,
96 astropy_to_arrow,
97 astropy_to_pandas,
98 compute_row_group_size,
99 numpy_dict_to_arrow,
100 numpy_to_arrow,
101 pandas_to_arrow,
102 pandas_to_astropy,
103 )
104except ImportError:
105 pa = None
106 pd = None
107 atable = None
108 np = None
109from lsst.daf.butler.tests._repo_template_cache import make_repo_for_test
110from lsst.daf.butler.tests.utils import makeTestTempDir, removeTestTempDir
111from lsst.resources.tests import make_remote_test_uri
113TESTDIR = os.path.abspath(os.path.dirname(__file__))
116def _makeSimpleNumpyTable(include_multidim=False, include_bigendian=False):
117 """Make a simple numpy table with random data.
119 Parameters
120 ----------
121 include_multidim : `bool`
122 Include multi-dimensional columns.
123 include_bigendian : `bool`
124 Include big-endian columns.
126 Returns
127 -------
128 numpyTable : `numpy.ndarray`
129 """
130 nrow = 5
132 dtype = [
133 ("index", "i4"),
134 ("a", "f8"),
135 ("b", "f8"),
136 ("c", "f8"),
137 ("ddd", "f8"),
138 ("f", "i8"),
139 ("strcol", "U10"),
140 ("bytecol", "S10"),
141 ("dtn", "datetime64[ns]"),
142 ("dtu", "datetime64[us]"),
143 ]
145 if include_multidim:
146 dtype.extend(
147 [
148 ("d1", "f4", (5,)),
149 ("d2", "i8", (5, 10)),
150 ("d3", "f8", (5, 10)),
151 ]
152 )
154 if include_bigendian:
155 dtype.extend([("a_bigendian", ">f8"), ("f_bigendian", ">i8")])
157 data = np.zeros(nrow, dtype=dtype)
158 data["index"][:] = np.arange(nrow)
159 data["a"] = np.random.randn(nrow)
160 data["b"] = np.random.randn(nrow)
161 data["c"] = np.random.randn(nrow)
162 data["ddd"] = np.random.randn(nrow)
163 data["f"] = np.arange(nrow) * 10
164 data["strcol"][:] = "teststring"
165 data["bytecol"][:] = "teststring"
166 data["dtn"] = datetime.datetime.fromisoformat("2024-07-23")
167 data["dtu"] = datetime.datetime.fromisoformat("2024-07-23")
169 if include_multidim:
170 data["d1"] = np.random.randn(data["d1"].size).reshape(data["d1"].shape)
171 data["d2"] = np.arange(data["d2"].size).reshape(data["d2"].shape)
172 data["d3"] = np.asfortranarray(np.random.randn(data["d3"].size).reshape(data["d3"].shape))
174 if include_bigendian:
175 data["a_bigendian"][:] = data["a"]
176 data["f_bigendian"][:] = data["f"]
178 return data
181def _makeSingleIndexDataFrame(include_masked=False, include_lists=False):
182 """Make a single index data frame for testing.
184 Parameters
185 ----------
186 include_masked : `bool`
187 Include masked columns.
188 include_lists : `bool`
189 Include list columns.
191 Returns
192 -------
193 dataFrame : `~pandas.DataFrame`
194 The test dataframe.
195 allColumns : `list` [`str`]
196 List of all the columns (including index columns).
197 """
198 data = _makeSimpleNumpyTable()
199 df = pd.DataFrame(data)
200 df = df.set_index("index")
202 if include_masked:
203 nrow = len(df)
205 df["m1"] = pd.array(np.arange(nrow), dtype=pd.Int64Dtype())
206 df["m2"] = pd.array(np.arange(nrow), dtype=np.float32)
207 df["mstrcol"] = pd.array(np.array(["text"] * nrow))
208 df.loc[1, ["m1", "m2", "mstrcol"]] = None
209 df.loc[0, "m1"] = 1649900760361600113
211 if include_lists:
212 nrow = len(df)
214 df["l1"] = [[0, 0]] * nrow
215 df["l2"] = [[0.0, 0.0]] * nrow
216 df["l3"] = [[]] * nrow
218 allColumns = df.columns.append(pd.Index(df.index.names))
220 return df, allColumns
223def _makeMultiIndexDataFrame():
224 """Make a multi-index data frame for testing.
226 Returns
227 -------
228 dataFrame : `~pandas.DataFrame`
229 The test dataframe.
230 """
231 columns = pd.MultiIndex.from_tuples(
232 [
233 ("g", "a"),
234 ("g", "b"),
235 ("g", "c"),
236 ("r", "a"),
237 ("r", "b"),
238 ("r", "c"),
239 ],
240 names=["filter", "column"],
241 )
242 df = pd.DataFrame(np.random.randn(5, 6), index=np.arange(5, dtype=int), columns=columns)
244 return df
247def _makeSimpleAstropyTable(include_multidim=False, include_masked=False, include_bigendian=False):
248 """Make an astropy table for testing.
250 Parameters
251 ----------
252 include_multidim : `bool`
253 Include multi-dimensional columns.
254 include_masked : `bool`
255 Include masked columns.
256 include_bigendian : `bool`
257 Include big-endian columns.
259 Returns
260 -------
261 astropyTable : `astropy.table.Table`
262 The test table.
263 """
264 data = _makeSimpleNumpyTable(include_multidim=include_multidim, include_bigendian=include_bigendian)
265 # Add a couple of units.
266 table = atable.Table(data)
267 table["a"].unit = units.degree
268 table["a"].description = "Description of column a"
269 table["b"].unit = units.meter
270 table["b"].description = "Description of column b"
272 # Add some masked columns.
273 if include_masked:
274 nrow = len(table)
275 mask = np.zeros(nrow, dtype=bool)
276 mask[1] = True
277 # We set the masked columns with the underlying sentinel value
278 # to be able test after serialization.
280 # Masked 64-bit integer.
281 arr = np.arange(nrow, dtype="i8")
282 arr[mask] = -1
283 arr[0] = 1649900760361600113
284 table["m_i8"] = np.ma.masked_array(data=arr, mask=mask, fill_value=-1)
285 # Masked 32-bit float.
286 arr = np.arange(nrow, dtype="f4")
287 arr[mask] = np.nan
288 table["m_f4"] = np.ma.masked_array(data=arr, mask=mask, fill_value=np.nan)
289 # Unmasked 32-bit float with NaNs.
290 table["um_f4"] = arr
291 # Masked 64-bit float.
292 arr = np.arange(nrow, dtype="f8")
293 arr[mask] = np.nan
294 table["m_f8"] = np.ma.masked_array(data=arr, mask=mask, fill_value=np.nan)
295 # Unmasked 64-bit float with NaNs.
296 table["um_f8"] = arr
297 # Masked boolean.
298 arr = np.zeros(nrow, dtype=np.bool_)
299 arr[mask] = True
300 table["m_bool"] = np.ma.masked_array(data=arr, mask=mask, fill_value=True)
301 # Masked unsigned 32-bit unsigned int.
302 arr = np.arange(nrow, dtype="u4")
303 arr[mask] = 0
304 table["m_u4"] = np.ma.masked_array(data=arr, mask=mask, fill_value=0)
305 # Masked string.
306 table["m_str"] = np.ma.masked_array(data=np.array(["text"] * nrow), mask=mask, fill_value="")
307 # Masked bytes.
308 table["m_byte"] = np.ma.masked_array(data=np.array([b"bytes"] * nrow), mask=mask, fill_value=b"")
310 return table
313def _makeSimpleArrowTable(include_multidim=False, include_masked=False):
314 """Make an arrow table for testing.
316 Parameters
317 ----------
318 include_multidim : `bool`
319 Include multi-dimensional columns.
320 include_masked : `bool`
321 Include masked columns.
323 Returns
324 -------
325 arrowTable : `pyarrow.Table`
326 The test table.
327 """
328 data = _makeSimpleAstropyTable(include_multidim=include_multidim, include_masked=include_masked)
329 return astropy_to_arrow(data)
332@unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.")
333@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterDataFrame without pyarrow.")
334class ParquetFormatterDataFrameTestCase(unittest.TestCase):
335 """Tests for ParquetFormatter, DataFrame, using local file datastore."""
337 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
339 def setUp(self):
340 """Create a new butler root for each test."""
341 self.root = makeTestTempDir(TESTDIR)
342 config = Config(self.configFile)
343 self.run = "test_run"
344 self.butler = Butler.from_config(
345 make_repo_for_test(self.root, config=config), writeable=True, run=self.run
346 )
347 self.enterContext(self.butler)
348 # No dimensions in dataset type so we don't have to worry about
349 # inserting dimension data or defining data IDs.
350 self.datasetType = DatasetType(
351 "data", dimensions=(), storageClass="DataFrame", universe=self.butler.dimensions
352 )
353 self.butler.registry.registerDatasetType(self.datasetType)
355 def tearDown(self):
356 removeTestTempDir(self.root)
358 def testSingleIndexDataFrame(self):
359 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True)
361 self.butler.put(df1, self.datasetType, dataId={})
362 # Read the whole DataFrame.
363 df2 = self.butler.get(self.datasetType, dataId={})
364 self.assertTrue(df1.equals(df2))
365 # Read just the column descriptions.
366 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
367 self.assertTrue(allColumns.equals(columns2))
368 # Read the rowcount.
369 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
370 self.assertEqual(rowcount, len(df1))
371 # Read the schema.
372 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
373 self.assertEqual(schema, DataFrameSchema(df1))
374 # Read just some columns a few different ways.
375 df3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
376 self.assertTrue(df1.loc[:, ["a", "c"]].equals(df3))
377 df4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
378 self.assertTrue(df1.loc[:, ["a"]].equals(df4))
379 df5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
380 self.assertTrue(df1.loc[:, ["a"]].equals(df5))
381 df6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
382 self.assertTrue(df1.loc[:, ["ddd"]].equals(df6))
383 df7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
384 self.assertTrue(df1.loc[:, ["a"]].equals(df7))
385 df8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d*"]})
386 self.assertTrue(df1.loc[:, ["ddd", "dtn", "dtu"]].equals(df8))
387 df9 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d*", "d*"]})
388 self.assertTrue(df1.loc[:, ["ddd", "dtn", "dtu"]].equals(df9))
389 # Passing an unrecognized column should be a ValueError.
390 with self.assertRaises(ValueError):
391 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
393 def testSingleIndexDataFrameWithLists(self):
394 df1, allColumns = _makeSingleIndexDataFrame(include_lists=True)
396 self.butler.put(df1, self.datasetType, dataId={})
397 # Read the whole DataFrame.
398 df2 = self.butler.get(self.datasetType, dataId={})
400 # We need to check the list columns specially because they go
401 # from lists to arrays.
402 for col in ["l1", "l2", "l3"]:
403 for i in range(len(df1)):
404 self.assertTrue(np.all(df2[col].values[i] == df1[col].values[i]))
406 def testMultiIndexDataFrame(self):
407 df1 = _makeMultiIndexDataFrame()
409 self.butler.put(df1, self.datasetType, dataId={})
410 # Read the whole DataFrame.
411 df2 = self.butler.get(self.datasetType, dataId={})
412 self.assertTrue(df1.equals(df2))
413 # Read just the column descriptions.
414 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
415 self.assertTrue(df1.columns.equals(columns2))
416 self.assertEqual(columns2.names, df1.columns.names)
417 # Read the rowcount.
418 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
419 self.assertEqual(rowcount, len(df1))
420 # Read the schema.
421 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
422 self.assertEqual(schema, DataFrameSchema(df1))
423 # Read just some columns a few different ways.
424 df3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": {"filter": "g"}})
425 self.assertTrue(df1.loc[:, ["g"]].equals(df3))
426 df4 = self.butler.get(
427 self.datasetType, dataId={}, parameters={"columns": {"filter": ["r"], "column": "a"}}
428 )
429 self.assertTrue(df1.loc[:, [("r", "a")]].equals(df4))
430 column_list = [("g", "a"), ("r", "c")]
431 df5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": column_list})
432 self.assertTrue(df1.loc[:, column_list].equals(df5))
433 column_dict = {"filter": "r", "column": ["a", "b"]}
434 df6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": column_dict})
435 self.assertTrue(df1.loc[:, [("r", "a"), ("r", "b")]].equals(df6))
436 # Passing an unrecognized column should be a ValueError.
437 with self.assertRaises(ValueError):
438 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d"]})
440 def testSingleIndexDataFrameEmptyString(self):
441 """Test persisting a single index dataframe with empty strings."""
442 df1, _ = _makeSingleIndexDataFrame()
444 # Set one of the strings to None
445 df1.at[1, "strcol"] = None
447 self.butler.put(df1, self.datasetType, dataId={})
448 # Read the whole DataFrame.
449 df2 = self.butler.get(self.datasetType, dataId={})
450 self.assertTrue(df1.equals(df2))
452 def testSingleIndexDataFrameAllEmptyStrings(self):
453 """Test persisting a single index dataframe with an empty string
454 column.
455 """
456 df1, _ = _makeSingleIndexDataFrame()
458 # Set all of the strings to None
459 df1.loc[0:, "strcol"] = None
461 self.butler.put(df1, self.datasetType, dataId={})
462 # Read the whole DataFrame.
463 df2 = self.butler.get(self.datasetType, dataId={})
464 self.assertTrue(df1.equals(df2))
466 def testLegacyDataFrame(self):
467 """Test writing a dataframe to parquet via pandas (without additional
468 metadata) and ensure that we can read it back with all the new
469 functionality.
470 """
471 df1, allColumns = _makeSingleIndexDataFrame()
473 if isinstance(df1.index, pd.RangeIndex): 473 ↛ 479line 473 didn't jump to line 479 because the condition on line 473 was never true
474 # Turn the RangeIndex into a regular index or it won't
475 # give us all the column names. This is necessary for pandas v3.
476 # Unfortunately, parquet files serialized directly with
477 # pandas v3 will not report their index column names if
478 # they are sequential integers.
479 df1.index = pd.Index(df1.index.to_numpy(), name=df1.index.name)
481 fname = os.path.join(self.root, "test_dataframe.parq")
482 df1.to_parquet(fname)
484 legacy_type = DatasetType(
485 "legacy_dataframe",
486 dimensions=(),
487 storageClass="DataFrame",
488 universe=self.butler.dimensions,
489 )
490 self.butler.registry.registerDatasetType(legacy_type)
492 data_id = {}
493 ref = DatasetRef(legacy_type, data_id, run=self.run)
494 dataset = FileDataset(path=fname, refs=[ref], formatter=ParquetFormatter)
496 self.butler.ingest(dataset, transfer="copy")
498 self.butler.put(df1, self.datasetType, dataId={})
500 df2a = self.butler.get(self.datasetType, dataId={})
501 df2b = self.butler.get("legacy_dataframe", dataId={})
502 self.assertTrue(df2a.equals(df2b))
504 df3a = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a"]})
505 df3b = self.butler.get("legacy_dataframe", dataId={}, parameters={"columns": ["a"]})
506 self.assertTrue(df3a.equals(df3b))
508 columns2a = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
509 columns2b = self.butler.get("legacy_dataframe.columns", dataId={})
510 self.assertTrue(columns2a.equals(columns2b))
512 rowcount2a = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
513 rowcount2b = self.butler.get("legacy_dataframe.rowcount", dataId={})
514 self.assertEqual(rowcount2a, rowcount2b)
516 schema2a = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
517 schema2b = self.butler.get("legacy_dataframe.schema", dataId={})
518 self.assertEqual(schema2a, schema2b)
520 def testDataFrameSchema(self):
521 tab1 = _makeSimpleArrowTable()
523 schema = DataFrameSchema.from_arrow(tab1.schema)
525 self.assertIsInstance(schema.schema, pd.DataFrame)
526 self.assertEqual(repr(schema), repr(schema._schema))
527 self.assertNotEqual(schema, "not_a_schema")
528 self.assertEqual(schema, schema)
530 tab2 = _makeMultiIndexDataFrame()
531 schema2 = DataFrameSchema(tab2)
533 self.assertNotEqual(schema, schema2)
535 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
536 def testWriteSingleIndexDataFrameReadAsAstropyTable(self):
537 df1, allColumns = _makeSingleIndexDataFrame()
539 self.butler.put(df1, self.datasetType, dataId={})
541 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
543 tab2_df = tab2.to_pandas(index="index")
544 self.assertTrue(df1.equals(tab2_df))
546 # Check reading the columns.
547 columns = list(tab2.columns.keys())
548 columns2 = self.butler.get(
549 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
550 )
551 # We check the set because pandas reorders the columns.
552 self.assertEqual(set(columns2), set(columns))
554 # Check reading the schema.
555 schema = ArrowAstropySchema(tab2)
556 schema2 = self.butler.get(
557 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema"
558 )
560 # The string types are objectified by pandas, and the order
561 # will be changed because of pandas indexing.
562 self.assertEqual(len(schema2.schema.columns), len(schema.schema.columns))
563 for name in schema.schema.columns:
564 self.assertIn(name, schema2.schema.columns)
565 if schema2.schema[name].dtype != np.dtype("O"):
566 self.assertEqual(schema2.schema[name].dtype, schema.schema[name].dtype)
568 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
569 def testWriteSingleIndexDataFrameWithMaskedColsReadAsAstropyTable(self):
570 # We need to special-case the write-as-pandas read-as-astropy code
571 # with masks because pandas has multiple ways to use masked columns.
572 # (The string column mask handling in particular is frustratingly
573 # inconsistent.)
574 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True)
576 self.butler.put(df1, self.datasetType, dataId={})
578 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
579 tab2_df = astropy_to_pandas(tab2, index="index")
581 self.assertTrue(df1.columns.equals(tab2_df.columns))
582 for name in tab2_df.columns:
583 col1 = df1[name]
584 col2 = tab2_df[name]
586 if col1.hasnans:
587 notNull = col1.notnull()
588 self.assertTrue(notNull.equals(col2.notnull()))
589 # Need to check value-by-value because column may
590 # be made of objects, depending on what pandas decides.
591 for index in notNull.values.nonzero()[0]:
592 self.assertEqual(col1[index], col2[index])
593 else:
594 self.assertTrue(col1.equals(col2))
596 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
597 def testWriteMultiIndexDataFrameReadAsAstropyTable(self):
598 df1 = _makeMultiIndexDataFrame()
600 self.butler.put(df1, self.datasetType, dataId={})
602 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
604 # This is an odd duck, it doesn't really round-trip.
605 # This test simply checks that it's readable, but definitely not
606 # recommended.
608 @unittest.skipUnless(atable is not None, "Cannot test writing as astropy without astropy.")
609 def testWriteAstropyTableWithMaskedColsReadAsSingleIndexDataFrame(self):
610 tab1 = _makeSimpleAstropyTable(include_masked=True)
612 self.butler.put(tab1, self.datasetType, dataId={})
614 tab2 = self.butler.get(self.datasetType, dataId={})
616 tab1_df = astropy_to_pandas(tab1)
617 self.assertTrue(tab1_df.equals(tab2))
619 tab2_astropy = pandas_to_astropy(tab2)
620 for col in tab1.dtype.names:
621 np.testing.assert_array_equal(tab2_astropy[col], tab1[col])
622 if isinstance(tab1[col], atable.column.MaskedColumn):
623 np.testing.assert_array_equal(tab2_astropy[col].mask, tab1[col].mask)
625 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
626 def testWriteSingleIndexDataFrameReadAsArrowTable(self):
627 df1, allColumns = _makeSingleIndexDataFrame()
629 self.butler.put(df1, self.datasetType, dataId={})
631 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
633 tab2_df = arrow_to_pandas(tab2)
634 self.assertTrue(df1.equals(tab2_df))
636 # Check reading the columns.
637 columns = list(tab2.schema.names)
638 columns2 = self.butler.get(
639 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
640 )
641 # We check the set because pandas reorders the columns.
642 self.assertEqual(set(columns), set(columns2))
644 # Override the component using a dataset type.
645 columnsType = self.datasetType.makeComponentDatasetType("columns").overrideStorageClass(
646 "ArrowColumnList"
647 )
648 self.assertEqual(columns2, self.butler.get(columnsType))
650 # Check getting a component while overriding the storage class via
651 # the dataset type. This overrides the parent storage class and then
652 # selects the component.
653 columnsType = self.datasetType.overrideStorageClass("ArrowAstropy").makeComponentDatasetType(
654 "columns"
655 )
656 self.assertEqual(columns2, self.butler.get(columnsType))
658 # Check reading the schema.
659 schema = tab2.schema
660 schema2 = self.butler.get(
661 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema"
662 )
664 # These will not have the same metadata, nor will the string column
665 # information be maintained.
666 self.assertEqual(len(schema.names), len(schema2.names))
667 for name in schema.names:
668 if schema.field(name).type not in (pa.string(), pa.binary()):
669 self.assertEqual(schema.field(name).type, schema2.field(name).type)
671 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
672 def testWriteMultiIndexDataFrameReadAsArrowTable(self):
673 df1 = _makeMultiIndexDataFrame()
675 self.butler.put(df1, self.datasetType, dataId={})
677 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
679 tab2_df = arrow_to_pandas(tab2)
680 self.assertTrue(df1.equals(tab2_df))
682 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
683 def testWriteSingleIndexDataFrameReadAsNumpyTable(self):
684 df1, allColumns = _makeSingleIndexDataFrame()
686 self.butler.put(df1, self.datasetType, dataId={})
688 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
690 tab2_df = pd.DataFrame.from_records(tab2, index=["index"])
691 self.assertTrue(df1.equals(tab2_df))
693 # Check reading the columns.
694 columns = list(tab2.dtype.names)
695 columns2 = self.butler.get(
696 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
697 )
698 # We check the set because pandas reorders the columns.
699 self.assertEqual(set(columns2), set(columns))
701 # Check reading the schema.
702 schema = ArrowNumpySchema(tab2.dtype)
703 schema2 = self.butler.get(
704 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema"
705 )
707 # The string types will be objectified by pandas, and the order
708 # will be changed because of pandas indexing.
709 self.assertEqual(len(schema.schema.names), len(schema2.schema.names))
710 for name in schema.schema.names:
711 self.assertIn(name, schema2.schema.names)
712 # It is not possible to properly track string columns via
713 # the schema consistently.
714 if schema.schema[name].type == np.dtype("O") or schema2.schema[name].type == np.dtype("O"):
715 continue
716 else:
717 self.assertEqual(schema2.schema[name].type, schema.schema[name].type)
719 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
720 def testWriteMultiIndexDataFrameReadAsNumpyTable(self):
721 df1 = _makeMultiIndexDataFrame()
723 self.butler.put(df1, self.datasetType, dataId={})
725 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
727 # This is an odd duck, it doesn't really round-trip.
728 # This test simply checks that it's readable, but definitely not
729 # recommended.
731 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.")
732 def testWriteSingleIndexDataFrameReadAsNumpyDict(self):
733 df1, allColumns = _makeSingleIndexDataFrame()
735 self.butler.put(df1, self.datasetType, dataId={})
737 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
739 tab2_df = pd.DataFrame.from_records(tab2, index=["index"])
740 # The column order is not maintained.
741 self.assertEqual(set(df1.columns), set(tab2_df.columns))
742 for col in df1.columns:
743 self.assertTrue(np.all(df1[col].values == tab2_df[col].values))
745 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.")
746 def testWriteMultiIndexDataFrameReadAsNumpyDict(self):
747 df1 = _makeMultiIndexDataFrame()
749 self.butler.put(df1, self.datasetType, dataId={})
751 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
753 # This is an odd duck, it doesn't really round-trip.
754 # This test simply checks that it's readable, but definitely not
755 # recommended.
757 def testBadDataFrameColumnParquet(self):
758 df1, allColumns = _makeSingleIndexDataFrame()
760 # Make a column with mixed type.
761 bad_col1 = [0.0] * len(df1)
762 bad_col1[1] = 0.0 * units.nJy
763 bad_df = df1.copy()
764 bad_df["bad_col1"] = bad_col1
766 # At the moment we cannot check that the correct note is added
767 # to the exception, but that will be possible in the future.
768 with self.assertRaises(RuntimeError):
769 self.butler.put(bad_df, self.datasetType, dataId={})
771 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
772 def testWriteReadAstropyTableLossless(self):
773 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
775 put_ref = self.butler.put(tab1, self.datasetType, dataId={})
777 tab2 = self.butler.get(
778 self.datasetType,
779 dataId={},
780 storageClass="ArrowAstropy",
781 parameters={"strip_astropy_meta_yaml": False},
782 )
784 # Check that minimal provenance was written by default.
785 expected = {
786 "LSST.BUTLER.ID": str(put_ref.id),
787 "LSST.BUTLER.RUN": "test_run",
788 "LSST.BUTLER.DATASETTYPE": "data",
789 "LSST.BUTLER.N_INPUTS": 0,
790 }
792 self.assertEqual(tab2.meta, expected)
794 _checkAstropyTableEquality(tab1, tab2)
796 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
797 def testWriteReadAstropyTableProvenance(self):
798 tab1 = _makeSimpleAstropyTable()
800 # Create a ref for provenance.
801 astropy_type = DatasetType(
802 "astropy_parquet",
803 dimensions=(),
804 storageClass="ArrowAstropy",
805 universe=self.butler.dimensions,
806 )
807 self.butler.registry.registerDatasetType(astropy_type)
808 input_ref = DatasetRef(astropy_type, {}, run="other_run")
809 quantum_id = uuid.uuid4()
810 provenance = DatasetProvenance(quantum_id=quantum_id)
811 provenance.add_input(input_ref)
813 put_ref = self.butler.put(tab1, self.datasetType, dataId={}, provenance=provenance)
815 tab2 = self.butler.get(
816 self.datasetType,
817 dataId={},
818 storageClass="ArrowAstropy",
819 parameters={"strip_astropy_meta_yaml": False},
820 )
822 expected = {
823 "LSST.BUTLER.ID": str(put_ref.id),
824 "LSST.BUTLER.RUN": "test_run",
825 "LSST.BUTLER.DATASETTYPE": "data",
826 "LSST.BUTLER.QUANTUM": str(quantum_id),
827 "LSST.BUTLER.N_INPUTS": 1,
828 "LSST.BUTLER.INPUT.0.ID": str(input_ref.id),
829 "LSST.BUTLER.INPUT.0.RUN": "other_run",
830 "LSST.BUTLER.INPUT.0.DATASETTYPE": "astropy_parquet",
831 }
832 self.assertEqual(tab2.meta, expected)
834 # Put the dataset again, with different provenance and ensure
835 # that the previous provenance was stripped.
836 self.butler.collections.register("new_run")
837 put_ref3 = self.butler.put(tab2, self.datasetType, dataId={}, run="new_run")
839 # tab2 will have been updated in place.
840 expected = {
841 "LSST.BUTLER.ID": str(put_ref3.id),
842 "LSST.BUTLER.RUN": "new_run",
843 "LSST.BUTLER.DATASETTYPE": "data",
844 "LSST.BUTLER.N_INPUTS": 0,
845 }
846 self.assertEqual(tab2.meta, expected)
847 null_prov, prov_ref = DatasetProvenance.from_flat_dict(tab2.meta, self.butler)
848 self.assertEqual(prov_ref, put_ref3)
849 self.assertEqual(null_prov, DatasetProvenance())
851 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
852 def testWriteReadNumpyTableLossless(self):
853 tab1 = _makeSimpleNumpyTable(include_multidim=True)
855 self.butler.put(tab1, self.datasetType, dataId={})
857 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
859 _checkNumpyTableEquality(tab1, tab2)
861 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
862 def testMaskedNumpy(self):
863 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=True)
864 tab1_np = arrow_to_numpy(tab1)
865 self.assertIsInstance(tab1_np, np.ma.MaskedArray)
866 # Stats on a masked column should ignore the nan in row 1.
867 col = tab1_np["m_f8"]
868 self.assertEqual(np.mean(col), 2.25, f"Column: {col}")
870 # Now without a mask.
871 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=False)
872 tab1_np = arrow_to_numpy(tab1)
873 self.assertNotIsInstance(tab1_np, np.ma.MaskedArray)
875 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
876 def testWriteReadArrowTableLossless(self):
877 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=True)
879 self.butler.put(tab1, self.datasetType, dataId={})
881 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
883 self.assertEqual(tab1.schema, tab2.schema)
884 tab1_np = arrow_to_numpy(tab1)
885 tab2_np = arrow_to_numpy(tab2)
886 for col in tab1.column_names:
887 np.testing.assert_array_equal(tab2_np[col], tab1_np[col])
889 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.")
890 def testWriteReadNumpyDictLossless(self):
891 tab1 = _makeSimpleNumpyTable(include_multidim=True)
892 dict1 = _numpy_to_numpy_dict(tab1)
894 self.butler.put(tab1, self.datasetType, dataId={})
896 dict2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
898 _checkNumpyDictEquality(dict1, dict2)
901@unittest.skipUnless(pd is not None, "Cannot test InMemoryDatastore with DataFrames without pandas.")
902class InMemoryDataFrameDelegateTestCase(ParquetFormatterDataFrameTestCase):
903 """Tests for InMemoryDatastore, using ArrowTableDelegate with Dataframe."""
905 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
907 def testBadDataFrameColumnParquet(self):
908 # This test does not raise for an in-memory datastore.
909 pass
911 def testWriteMultiIndexDataFrameReadAsAstropyTable(self):
912 df1 = _makeMultiIndexDataFrame()
914 self.butler.put(df1, self.datasetType, dataId={})
916 with self.assertRaises(ValueError):
917 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
919 def testLegacyDataFrame(self):
920 # This test does not work with an inMemoryDatastore.
921 pass
923 def testBadInput(self):
924 df1, _ = _makeSingleIndexDataFrame()
925 delegate = ArrowTableDelegate("DataFrame")
927 with self.assertRaises(ValueError):
928 delegate.handleParameters(inMemoryDataset="not_a_dataframe")
930 with self.assertRaises(AttributeError):
931 delegate.getComponent(composite=df1, componentName="nothing")
933 def testStorageClass(self):
934 df1, allColumns = _makeSingleIndexDataFrame()
936 factory = StorageClassFactory()
937 factory.addFromConfig(StorageClassConfig())
939 storageClass = factory.findStorageClass(type(df1), compare_types=False)
940 # Force the name lookup to do name matching.
941 storageClass._pytype = None
942 self.assertEqual(storageClass.name, "DataFrame")
944 storageClass = factory.findStorageClass(type(df1), compare_types=True)
945 # Force the name lookup to do name matching.
946 storageClass._pytype = None
947 self.assertEqual(storageClass.name, "DataFrame")
950@unittest.skipUnless(atable is not None, "Cannot test ParquetFormatterArrowAstropy without astropy.")
951@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowAstropy without pyarrow.")
952class ParquetFormatterArrowAstropyTestCase(unittest.TestCase):
953 """Tests for ParquetFormatter, ArrowAstropy, using local file datastore."""
955 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
957 def setUp(self):
958 """Create a new butler root for each test."""
959 self.root = makeTestTempDir(TESTDIR)
960 config = Config(self.configFile)
961 self.run = "test_run"
962 self.butler = Butler.from_config(
963 make_repo_for_test(self.root, config=config), writeable=True, run=self.run
964 )
965 self.enterContext(self.butler)
966 # No dimensions in dataset type so we don't have to worry about
967 # inserting dimension data or defining data IDs.
968 self.datasetType = DatasetType(
969 "data", dimensions=(), storageClass="ArrowAstropy", universe=self.butler.dimensions
970 )
971 self.butler.registry.registerDatasetType(self.datasetType)
973 def tearDown(self):
974 removeTestTempDir(self.root)
976 def testAstropyTable(self):
977 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
979 self.butler.put(tab1, self.datasetType, dataId={})
980 # Read the whole Table.
981 tab2 = self.butler.get(self.datasetType, dataId={})
982 _checkAstropyTableEquality(tab1, tab2)
983 # Read the columns.
984 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
985 self.assertEqual(len(columns2), len(tab1.dtype.names))
986 for i, name in enumerate(tab1.dtype.names):
987 self.assertEqual(columns2[i], name)
988 # Read the rowcount.
989 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
990 self.assertEqual(rowcount, len(tab1))
991 # Read the schema.
992 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
993 self.assertEqual(schema, ArrowAstropySchema(tab1))
994 # Read just some columns a few different ways.
995 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
996 _checkAstropyTableEquality(tab1[("a", "c")], tab3)
997 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
998 _checkAstropyTableEquality(tab1[("a",)], tab4)
999 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
1000 _checkAstropyTableEquality(tab1[("index", "a")], tab5)
1001 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
1002 _checkAstropyTableEquality(tab1[("ddd",)], tab6)
1003 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
1004 _checkAstropyTableEquality(tab1[("a",)], tab7)
1005 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??"]})
1006 _checkAstropyTableEquality(tab1[("ddd", "dtn", "dtu")], tab8)
1007 tab9 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]})
1008 _checkAstropyTableEquality(tab1[("ddd", "dtn", "dtu", "a")], tab9)
1009 # Passing an unrecognized column should be a ValueError.
1010 with self.assertRaises(ValueError):
1011 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
1013 def testAstropyTableBigEndian(self):
1014 tab1 = _makeSimpleAstropyTable(include_bigendian=True)
1016 self.butler.put(tab1, self.datasetType, dataId={})
1017 # Read the whole Table.
1018 tab2 = self.butler.get(self.datasetType, dataId={})
1019 _checkAstropyTableEquality(tab1, tab2, has_bigendian=True)
1021 def testAstropyTableWithMetadata(self):
1022 tab1 = _makeSimpleAstropyTable(include_multidim=True)
1024 meta = {
1025 "meta_a": 5,
1026 "meta_b": 10.0,
1027 "meta_c": [1, 2, 3],
1028 "meta_d": True,
1029 "meta_e": "string",
1030 }
1032 tab1.meta.update(meta)
1034 self.butler.put(tab1, self.datasetType, dataId={})
1035 # Read the whole Table.
1036 tab2 = self.butler.get(self.datasetType, dataId={}, parameters={"strip_astropy_meta_yaml": False})
1037 # This will check that the metadata is equivalent as well.
1038 _checkAstropyTableEquality(tab1, tab2)
1040 def testArrowAstropySchema(self):
1041 tab1 = _makeSimpleAstropyTable()
1042 tab1_arrow = astropy_to_arrow(tab1)
1043 schema = ArrowAstropySchema.from_arrow(tab1_arrow.schema)
1045 self.assertIsInstance(schema.schema, atable.Table)
1046 self.assertEqual(repr(schema), repr(schema._schema))
1047 self.assertNotEqual(schema, "not_a_schema")
1048 self.assertEqual(schema, schema)
1050 # Test various inequalities
1051 tab2 = tab1.copy()
1052 tab2.rename_column("index", "index2")
1053 schema2 = ArrowAstropySchema(tab2)
1054 self.assertNotEqual(schema2, schema)
1056 tab2 = tab1.copy()
1057 tab2["index"].unit = units.micron
1058 schema2 = ArrowAstropySchema(tab2)
1059 self.assertNotEqual(schema2, schema)
1061 tab2 = tab1.copy()
1062 tab2["index"].description = "Index column"
1063 schema2 = ArrowAstropySchema(tab2)
1064 self.assertNotEqual(schema2, schema)
1066 tab2 = tab1.copy()
1067 tab2["index"].format = "%05d"
1068 schema2 = ArrowAstropySchema(tab2)
1069 self.assertNotEqual(schema2, schema)
1071 def testAstropyParquet(self):
1072 tab1 = _makeSimpleAstropyTable()
1074 # Remove datetime column which doesn't work with astropy currently.
1075 del tab1["dtn"]
1076 del tab1["dtu"]
1078 fname = os.path.join(self.root, "test_astropy.parq")
1079 tab1.write(fname)
1081 astropy_type = DatasetType(
1082 "astropy_parquet",
1083 dimensions=(),
1084 storageClass="ArrowAstropy",
1085 universe=self.butler.dimensions,
1086 )
1087 self.butler.registry.registerDatasetType(astropy_type)
1089 data_id = {}
1090 ref = DatasetRef(astropy_type, data_id, run=self.run)
1091 dataset = FileDataset(path=fname, refs=[ref], formatter=ParquetFormatter)
1093 self.butler.ingest(dataset, transfer="copy")
1095 self.butler.put(tab1, self.datasetType, dataId={})
1097 tab2a = self.butler.get(self.datasetType, dataId={})
1098 tab2b = self.butler.get("astropy_parquet", dataId={})
1099 _checkAstropyTableEquality(tab2a, tab2b)
1101 columns2a = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1102 columns2b = self.butler.get("astropy_parquet.columns", dataId={})
1103 self.assertEqual(len(columns2b), len(columns2a))
1104 for i, name in enumerate(columns2a):
1105 self.assertEqual(columns2b[i], name)
1107 rowcount2a = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
1108 rowcount2b = self.butler.get("astropy_parquet.rowcount", dataId={})
1109 self.assertEqual(rowcount2a, rowcount2b)
1111 schema2a = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
1112 schema2b = self.butler.get("astropy_parquet.schema", dataId={})
1113 self.assertEqual(schema2a, schema2b)
1115 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
1116 def testWriteAstropyReadAsArrowTable(self):
1117 # This astropy <-> arrow works fine with masked columns.
1118 tab1 = _makeSimpleAstropyTable(include_masked=True)
1120 self.butler.put(tab1, self.datasetType, dataId={})
1122 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
1124 tab2_astropy = arrow_to_astropy(tab2)
1125 _checkAstropyTableEquality(tab1, tab2_astropy)
1127 # Check reading the columns.
1128 columns = tab2.schema.names
1129 columns2 = self.butler.get(
1130 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1131 )
1132 self.assertEqual(columns2, columns)
1134 # Check reading the schema.
1135 schema = tab2.schema
1136 schema2 = self.butler.get(
1137 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema"
1138 )
1140 self.assertEqual(schema, schema2)
1142 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1143 def testWriteAstropyReadAsDataFrame(self):
1144 tab1 = _makeSimpleAstropyTable()
1146 self.butler.put(tab1, self.datasetType, dataId={})
1148 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1150 # This is tricky because it loses the units and gains a bonus pandas
1151 # _index_ column, so we just test the dataframe form.
1153 tab1_df = tab1.to_pandas()
1154 self.assertTrue(tab1_df.equals(tab2))
1156 # Check reading the columns.
1157 columns = tab2.columns
1158 columns2 = self.butler.get(
1159 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1160 )
1161 self.assertTrue(columns.equals(columns2))
1163 # Check reading the schema.
1164 schema = DataFrameSchema(tab2)
1165 schema2 = self.butler.get(
1166 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1167 )
1169 self.assertEqual(schema2, schema)
1171 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1172 def testWriteAstropyWithMaskedColsReadAsDataFrame(self):
1173 # We need to special-case the write-as-astropy read-as-pandas code
1174 # with masks because pandas has multiple ways to use masked columns.
1175 # (When writing an astropy table with masked columns we get an object
1176 # column back, but each unmasked element has the correct type.)
1177 tab1 = _makeSimpleAstropyTable(include_masked=True)
1179 self.butler.put(tab1, self.datasetType, dataId={})
1181 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1183 tab1_df = astropy_to_pandas(tab1)
1185 self.assertTrue(tab1_df.columns.equals(tab2.columns))
1186 for name in tab2.columns:
1187 col1 = tab1_df[name]
1188 col2 = tab2[name]
1190 if col1.hasnans:
1191 notNull = col1.notnull()
1192 self.assertTrue(notNull.equals(col2.notnull()))
1193 # Need to check value-by-value because column may
1194 # be made of objects, depending on what pandas decides.
1195 for index in notNull.values.nonzero()[0]:
1196 self.assertEqual(col1[index], col2[index])
1197 else:
1198 self.assertTrue(col1.equals(col2))
1200 @unittest.skipUnless(pd is not None, "Cannot test writing as a dataframe without pandas.")
1201 def testWriteSingleIndexDataFrameWithMaskedColsReadAsAstropyTable(self):
1202 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True)
1204 self.butler.put(df1, self.datasetType, dataId={})
1206 tab2 = self.butler.get(self.datasetType, dataId={})
1208 df1_tab = pandas_to_astropy(df1)
1210 _checkAstropyTableEquality(df1_tab, tab2)
1212 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1213 def testWriteAstropyReadAsNumpyTable(self):
1214 tab1 = _makeSimpleAstropyTable()
1215 self.butler.put(tab1, self.datasetType, dataId={})
1217 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
1219 # This is tricky because it loses the units.
1220 tab2_astropy = atable.Table(tab2)
1222 _checkAstropyTableEquality(tab1, tab2_astropy, skip_units=True)
1224 # Check reading the columns.
1225 columns = list(tab2.dtype.names)
1226 columns2 = self.butler.get(
1227 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1228 )
1229 self.assertEqual(columns2, columns)
1231 # Check reading the schema.
1232 schema = ArrowNumpySchema(tab2.dtype)
1233 schema2 = self.butler.get(
1234 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema"
1235 )
1237 self.assertEqual(schema2, schema)
1239 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1240 def testWriteAstropyReadAsNumpyDict(self):
1241 tab1 = _makeSimpleAstropyTable()
1242 self.butler.put(tab1, self.datasetType, dataId={})
1244 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
1246 # This is tricky because it loses the units.
1247 tab2_astropy = atable.Table(tab2)
1249 _checkAstropyTableEquality(tab1, tab2_astropy, skip_units=True)
1251 def testBadAstropyColumnParquet(self):
1252 tab1 = _makeSimpleAstropyTable()
1254 # Make a column with mixed type.
1255 bad_col1 = [0.0] * len(tab1)
1256 bad_col1[1] = 0.0 * units.nJy
1257 bad_tab = tab1.copy()
1258 bad_tab["bad_col1"] = bad_col1
1260 # At the moment we cannot check that the correct note is added
1261 # to the exception, but that will be possible in the future.
1262 with self.assertRaises(RuntimeError):
1263 self.butler.put(bad_tab, self.datasetType, dataId={})
1265 # Make a column with ragged size.
1266 bad_col2 = [[0]] * len(tab1)
1267 bad_col2[1] = [0, 0]
1268 bad_tab = tab1.copy()
1269 bad_tab["bad_col2"] = bad_col2
1271 with self.assertRaises(RuntimeError):
1272 self.butler.put(bad_tab, self.datasetType, dataId={})
1274 @unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.")
1275 def testWriteAstropyTableWithPandasIndexHint(self, testStrip=True):
1276 tab1 = _makeSimpleAstropyTable()
1278 add_pandas_index_to_astropy(tab1, "index")
1280 self.butler.put(tab1, self.datasetType, dataId={})
1282 # Read in as an astropy table and ensure index hint is still there.
1283 tab2 = self.butler.get(self.datasetType, dataId={})
1285 self.assertIn(ASTROPY_PANDAS_INDEX_KEY, tab2.meta)
1286 self.assertEqual(tab2.meta[ASTROPY_PANDAS_INDEX_KEY], "index")
1288 # Read as a dataframe and ensure index is set.
1289 df3 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1291 self.assertEqual(df3.index.name, "index")
1293 # Read as a dataframe without naming the index column.
1294 with self.assertLogs(level="WARNING") as cm:
1295 _ = self.butler.get(
1296 self.datasetType,
1297 dataId={},
1298 storageClass="DataFrame",
1299 parameters={"columns": ["a", "b"]},
1300 )
1301 self.assertIn("Index column ``index``", cm.output[0])
1303 if testStrip:
1304 # Read as an astropy table without naming the index column.
1305 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "b"]})
1307 self.assertNotIn(ASTROPY_PANDAS_INDEX_KEY, tab5.meta)
1309 with self.assertRaises(ValueError):
1310 add_pandas_index_to_astropy(tab1, "not_a_column")
1313@unittest.skipUnless(atable is not None, "Cannot test InMemoryDatastore with AstropyTable without astropy.")
1314class InMemoryArrowAstropyDelegateTestCase(ParquetFormatterArrowAstropyTestCase):
1315 """Tests for InMemoryDatastore, using ArrowTableDelegate with
1316 AstropyTable.
1317 """
1319 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
1321 def testAstropyParquet(self):
1322 # This test does not work with an inMemoryDatastore.
1323 pass
1325 def testBadAstropyColumnParquet(self):
1326 # This test does not raise for an in-memory datastore.
1327 pass
1329 def testBadInput(self):
1330 tab1 = _makeSimpleAstropyTable()
1331 delegate = ArrowTableDelegate("ArrowAstropy")
1333 with self.assertRaises(ValueError):
1334 delegate.handleParameters(inMemoryDataset="not_an_astropy_table")
1336 with self.assertRaises(NotImplementedError):
1337 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]})
1339 with self.assertRaises(AttributeError):
1340 delegate.getComponent(composite=tab1, componentName="nothing")
1342 @unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.")
1343 def testWriteAstropyTableWithPandasIndexHint(self):
1344 super().testWriteAstropyTableWithPandasIndexHint(testStrip=False)
1347@unittest.skipUnless(np is not None, "Cannot test ParquetFormatterArrowNumpy without numpy.")
1348@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowNumpy without pyarrow.")
1349class ParquetFormatterArrowNumpyTestCase(unittest.TestCase):
1350 """Tests for ParquetFormatter, ArrowNumpy, using local file datastore."""
1352 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
1354 def setUp(self):
1355 """Create a new butler root for each test."""
1356 self.root = makeTestTempDir(TESTDIR)
1357 config = Config(self.configFile)
1358 self.butler = Butler.from_config(
1359 make_repo_for_test(self.root, config=config), writeable=True, run="test_run"
1360 )
1361 self.enterContext(self.butler)
1362 # No dimensions in dataset type so we don't have to worry about
1363 # inserting dimension data or defining data IDs.
1364 self.datasetType = DatasetType(
1365 "data", dimensions=(), storageClass="ArrowNumpy", universe=self.butler.dimensions
1366 )
1367 self.butler.registry.registerDatasetType(self.datasetType)
1369 def tearDown(self):
1370 removeTestTempDir(self.root)
1372 def testNumpyTable(self):
1373 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1375 self.butler.put(tab1, self.datasetType, dataId={})
1376 # Read the whole Table.
1377 tab2 = self.butler.get(self.datasetType, dataId={})
1378 _checkNumpyTableEquality(tab1, tab2)
1379 # Read the columns.
1380 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1381 self.assertEqual(len(columns2), len(tab1.dtype.names))
1382 for i, name in enumerate(tab1.dtype.names):
1383 self.assertEqual(columns2[i], name)
1384 # Read the rowcount.
1385 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
1386 self.assertEqual(rowcount, len(tab1))
1387 # Read the schema.
1388 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
1389 self.assertEqual(schema, ArrowNumpySchema(tab1.dtype))
1390 # Read just some columns a few different ways.
1391 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
1392 _checkNumpyTableEquality(tab1[["a", "c"]], tab3)
1393 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
1394 _checkNumpyTableEquality(
1395 tab1[
1396 [
1397 "a",
1398 ]
1399 ],
1400 tab4,
1401 )
1402 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
1403 _checkNumpyTableEquality(tab1[["index", "a"]], tab5)
1404 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
1405 _checkNumpyTableEquality(
1406 tab1[
1407 [
1408 "ddd",
1409 ]
1410 ],
1411 tab6,
1412 )
1413 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
1414 _checkNumpyTableEquality(
1415 tab1[
1416 [
1417 "a",
1418 ]
1419 ],
1420 tab7,
1421 )
1422 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]})
1423 _checkNumpyTableEquality(
1424 tab1[
1425 [
1426 "ddd",
1427 "dtn",
1428 "dtu",
1429 "a",
1430 ]
1431 ],
1432 tab8,
1433 )
1434 # Passing an unrecognized column should be a ValueError.
1435 with self.assertRaises(ValueError):
1436 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
1438 def testNumpyTableBigEndian(self):
1439 tab1 = _makeSimpleNumpyTable(include_bigendian=True)
1441 self.butler.put(tab1, self.datasetType, dataId={})
1442 # Read the whole Table.
1443 tab2 = self.butler.get(self.datasetType, dataId={})
1444 _checkNumpyTableEquality(tab1, tab2, has_bigendian=True)
1446 def testArrowNumpySchema(self):
1447 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1448 tab1_arrow = numpy_to_arrow(tab1)
1449 schema = ArrowNumpySchema.from_arrow(tab1_arrow.schema)
1451 self.assertIsInstance(schema.schema, np.dtype)
1452 self.assertEqual(repr(schema), repr(schema._dtype))
1453 self.assertNotEqual(schema, "not_a_schema")
1454 self.assertEqual(schema, schema)
1456 # Test inequality
1457 tab2 = tab1.copy()
1458 names = list(tab2.dtype.names)
1459 names[0] = "index2"
1460 tab2.dtype.names = names
1461 schema2 = ArrowNumpySchema(tab2.dtype)
1462 self.assertNotEqual(schema2, schema)
1464 @unittest.skipUnless(pa is not None, "Cannot test arrow conversions without pyarrow.")
1465 def testNumpyDictConversions(self):
1466 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1468 # Verify that everything round-trips, including the schema.
1469 tab1_arrow = numpy_to_arrow(tab1)
1470 tab1_dict = arrow_to_numpy_dict(tab1_arrow)
1471 tab1_dict_arrow = numpy_dict_to_arrow(tab1_dict)
1473 self.assertEqual(tab1_arrow.schema, tab1_dict_arrow.schema)
1474 self.assertEqual(tab1_arrow, tab1_dict_arrow)
1476 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
1477 def testWriteNumpyTableReadAsArrowTable(self):
1478 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1480 self.butler.put(tab1, self.datasetType, dataId={})
1482 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
1484 tab2_numpy = arrow_to_numpy(tab2)
1486 _checkNumpyTableEquality(tab1, tab2_numpy)
1488 # Check reading the columns.
1489 columns = tab2.schema.names
1490 columns2 = self.butler.get(
1491 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1492 )
1493 self.assertEqual(columns2, columns)
1495 # Check reading the schema.
1496 schema = tab2.schema
1497 schema2 = self.butler.get(
1498 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema"
1499 )
1500 self.assertEqual(schema2, schema)
1502 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1503 def testWriteNumpyTableReadAsDataFrame(self):
1504 tab1 = _makeSimpleNumpyTable()
1506 self.butler.put(tab1, self.datasetType, dataId={})
1508 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1510 # Converting this back to numpy gets confused with the index column
1511 # and changes the datatype of the string column.
1513 tab1_df = pd.DataFrame(tab1)
1515 self.assertTrue(tab1_df.equals(tab2))
1517 # Check reading the columns.
1518 columns = tab2.columns
1519 columns2 = self.butler.get(
1520 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1521 )
1522 self.assertTrue(columns.equals(columns2))
1524 # Check reading the schema.
1525 schema = DataFrameSchema(tab2)
1526 schema2 = self.butler.get(
1527 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1528 )
1530 self.assertEqual(schema2, schema)
1532 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1533 def testWriteNumpyTableReadAsAstropyTable(self):
1534 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1536 self.butler.put(tab1, self.datasetType, dataId={})
1538 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1539 tab2_numpy = tab2.as_array()
1541 _checkNumpyTableEquality(tab1, tab2_numpy)
1543 # Check reading the columns.
1544 columns = list(tab2.columns.keys())
1545 columns2 = self.butler.get(
1546 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1547 )
1548 self.assertEqual(columns2, columns)
1550 # Check reading the schema.
1551 schema = ArrowAstropySchema(tab2)
1552 schema2 = self.butler.get(
1553 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema"
1554 )
1556 self.assertEqual(schema2, schema)
1558 def testWriteNumpyTableReadAsNumpyDict(self):
1559 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1561 self.butler.put(tab1, self.datasetType, dataId={})
1563 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
1564 tab2_numpy = _numpy_dict_to_numpy(tab2)
1566 _checkNumpyTableEquality(tab1, tab2_numpy)
1568 def testBadNumpyColumnParquet(self):
1569 tab1 = _makeSimpleAstropyTable()
1571 # Make a column with mixed type.
1572 bad_col1 = [0.0] * len(tab1)
1573 bad_col1[1] = 0.0 * units.nJy
1574 bad_tab = tab1.copy()
1575 bad_tab["bad_col1"] = bad_col1
1577 bad_tab_np = bad_tab.as_array()
1579 # At the moment we cannot check that the correct note is added
1580 # to the exception, but that will be possible in the future.
1581 with self.assertRaises(RuntimeError):
1582 self.butler.put(bad_tab_np, self.datasetType, dataId={})
1584 # Make a column with ragged size.
1585 bad_col2 = [[0]] * len(tab1)
1586 bad_col2[1] = [0, 0]
1587 bad_tab = tab1.copy()
1588 bad_tab["bad_col2"] = bad_col2
1590 bad_tab_np = bad_tab.as_array()
1592 with self.assertRaises(RuntimeError):
1593 self.butler.put(bad_tab_np, self.datasetType, dataId={})
1595 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1596 def testWriteReadAstropyTableLossless(self):
1597 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
1599 self.butler.put(tab1, self.datasetType, dataId={})
1601 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1603 _checkAstropyTableEquality(tab1, tab2)
1606@unittest.skipUnless(np is not None, "Cannot test ImMemoryDatastore with Numpy table without numpy.")
1607class InMemoryArrowNumpyDelegateTestCase(ParquetFormatterArrowNumpyTestCase):
1608 """Tests for InMemoryDatastore, using ArrowTableDelegate with
1609 Numpy table.
1610 """
1612 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
1614 def testBadNumpyColumnParquet(self):
1615 # This test does not raise for an in-memory datastore.
1616 pass
1618 def testBadInput(self):
1619 tab1 = _makeSimpleNumpyTable()
1620 delegate = ArrowTableDelegate("ArrowNumpy")
1622 with self.assertRaises(ValueError):
1623 delegate.handleParameters(inMemoryDataset="not_a_numpy_table")
1625 with self.assertRaises(NotImplementedError):
1626 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]})
1628 with self.assertRaises(AttributeError):
1629 delegate.getComponent(composite=tab1, componentName="nothing")
1631 def testStorageClass(self):
1632 tab1 = _makeSimpleNumpyTable()
1634 factory = StorageClassFactory()
1635 factory.addFromConfig(StorageClassConfig())
1637 storageClass = factory.findStorageClass(type(tab1), compare_types=False)
1638 # Force the name lookup to do name matching.
1639 storageClass._pytype = None
1640 self.assertEqual(storageClass.name, "ArrowNumpy")
1642 storageClass = factory.findStorageClass(type(tab1), compare_types=True)
1643 # Force the name lookup to do name matching.
1644 storageClass._pytype = None
1645 self.assertEqual(storageClass.name, "ArrowNumpy")
1648@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowTable without pyarrow.")
1649class ParquetFormatterArrowTableTestCase(unittest.TestCase):
1650 """Tests for ParquetFormatter, ArrowTable, using local file datastore."""
1652 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
1654 def setUp(self):
1655 """Create a new butler root for each test."""
1656 self.root = makeTestTempDir(TESTDIR)
1657 config = Config(self.configFile)
1658 self.butler = Butler.from_config(
1659 make_repo_for_test(self.root, config=config), writeable=True, run="test_run"
1660 )
1661 self.enterContext(self.butler)
1662 # No dimensions in dataset type so we don't have to worry about
1663 # inserting dimension data or defining data IDs.
1664 self.datasetType = DatasetType(
1665 "data", dimensions=(), storageClass="ArrowTable", universe=self.butler.dimensions
1666 )
1667 self.butler.registry.registerDatasetType(self.datasetType)
1669 def tearDown(self):
1670 removeTestTempDir(self.root)
1672 def testArrowTable(self):
1673 tab1 = _makeSimpleArrowTable(include_multidim=True, include_masked=True)
1675 self.butler.put(tab1, self.datasetType, dataId={})
1676 # Read the whole Table.
1677 tab2 = self.butler.get(self.datasetType, dataId={})
1678 # We convert to use the numpy testing framework to handle nan
1679 # comparisons.
1680 self.assertEqual(tab1.schema, tab2.schema)
1681 tab1_np = arrow_to_numpy(tab1)
1682 tab2_np = arrow_to_numpy(tab2)
1683 for col in tab1.column_names:
1684 np.testing.assert_array_equal(tab2_np[col], tab1_np[col])
1685 # Read the columns.
1686 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1687 self.assertEqual(len(columns2), len(tab1.schema.names))
1688 for i, name in enumerate(tab1.schema.names):
1689 self.assertEqual(columns2[i], name)
1690 # Read the rowcount.
1691 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
1692 self.assertEqual(rowcount, len(tab1))
1693 # Read the schema.
1694 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
1695 self.assertEqual(schema, tab1.schema)
1696 # Read just some columns a few different ways.
1697 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
1698 self.assertEqual(tab3, tab1.select(("a", "c")))
1699 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
1700 self.assertEqual(tab4, tab1.select(("a",)))
1701 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
1702 self.assertEqual(tab5, tab1.select(("index", "a")))
1703 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
1704 self.assertEqual(tab6, tab1.select(("ddd",)))
1705 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
1706 self.assertEqual(tab7, tab1.select(("a",)))
1707 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a*", "d??"]})
1708 self.assertEqual(tab8, tab1.select(("a", "ddd", "dtn", "dtu")))
1709 # Passing an unrecognized column should be a ValueError.
1710 with self.assertRaises(ValueError):
1711 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
1713 def testEmptyArrowTable(self):
1714 data = _makeSimpleNumpyTable()
1715 type_list = _numpy_dtype_to_arrow_types(data.dtype)
1717 schema = pa.schema(type_list)
1718 arrays = [[]] * len(schema.names)
1720 tab1 = pa.Table.from_arrays(arrays, schema=schema)
1722 self.butler.put(tab1, self.datasetType, dataId={})
1723 tab2 = self.butler.get(self.datasetType, dataId={})
1724 self.assertEqual(tab2, tab1)
1726 tab1_numpy = arrow_to_numpy(tab1)
1727 self.assertEqual(len(tab1_numpy), 0)
1728 tab1_numpy_arrow = numpy_to_arrow(tab1_numpy)
1729 self.assertEqual(tab1_numpy_arrow, tab1)
1731 tab1_pandas = arrow_to_pandas(tab1)
1732 self.assertEqual(len(tab1_pandas), 0)
1733 tab1_pandas_arrow = pandas_to_arrow(tab1_pandas)
1734 # Unfortunately, string/byte columns get mangled when translated
1735 # through empty pandas dataframes.
1736 self.assertEqual(
1737 tab1_pandas_arrow.select(("index", "a", "b", "c", "ddd")),
1738 tab1.select(("index", "a", "b", "c", "ddd")),
1739 )
1741 tab1_astropy = arrow_to_astropy(tab1)
1742 self.assertEqual(len(tab1_astropy), 0)
1743 tab1_astropy_arrow = astropy_to_arrow(tab1_astropy)
1744 self.assertEqual(tab1_astropy_arrow, tab1)
1746 def testEmptyArrowTableMultidim(self):
1747 data = _makeSimpleNumpyTable(include_multidim=True)
1748 type_list = _numpy_dtype_to_arrow_types(data.dtype)
1750 md = {}
1751 for name in data.dtype.names:
1752 _append_numpy_multidim_metadata(md, name, data.dtype[name])
1754 schema = pa.schema(type_list, metadata=md)
1755 arrays = [[]] * len(schema.names)
1757 tab1 = pa.Table.from_arrays(arrays, schema=schema)
1759 self.butler.put(tab1, self.datasetType, dataId={})
1760 tab2 = self.butler.get(self.datasetType, dataId={})
1761 self.assertEqual(tab2, tab1)
1763 tab1_numpy = arrow_to_numpy(tab1)
1764 self.assertEqual(len(tab1_numpy), 0)
1765 tab1_numpy_arrow = numpy_to_arrow(tab1_numpy)
1766 self.assertEqual(tab1_numpy_arrow, tab1)
1768 tab1_astropy = arrow_to_astropy(tab1)
1769 self.assertEqual(len(tab1_astropy), 0)
1770 tab1_astropy_arrow = astropy_to_arrow(tab1_astropy)
1771 self.assertEqual(tab1_astropy_arrow, tab1)
1773 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1774 def testWriteArrowTableReadAsSingleIndexDataFrame(self):
1775 df1, allColumns = _makeSingleIndexDataFrame()
1777 self.butler.put(df1, self.datasetType, dataId={})
1779 # Read back out as a dataframe.
1780 df2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1781 self.assertTrue(df1.equals(df2))
1783 # Read back out as an arrow table, convert to dataframe.
1784 tab3 = self.butler.get(self.datasetType, dataId={})
1785 df3 = arrow_to_pandas(tab3)
1786 self.assertTrue(df1.equals(df3))
1788 # Check reading the columns.
1789 columns = df2.reset_index().columns
1790 columns2 = self.butler.get(
1791 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1792 )
1793 # We check the set because pandas reorders the columns.
1794 self.assertEqual(set(columns2.to_list()), set(columns.to_list()))
1796 # Check reading the schema.
1797 schema = DataFrameSchema(df1)
1798 schema2 = self.butler.get(
1799 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1800 )
1801 self.assertEqual(schema2, schema)
1803 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1804 def testWriteArrowTableReadAsMultiIndexDataFrame(self):
1805 df1 = _makeMultiIndexDataFrame()
1807 self.butler.put(df1, self.datasetType, dataId={})
1809 # Read back out as a dataframe.
1810 df2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1811 self.assertTrue(df1.equals(df2))
1813 # Read back out as an arrow table, convert to dataframe.
1814 atab3 = self.butler.get(self.datasetType, dataId={})
1815 df3 = arrow_to_pandas(atab3)
1816 self.assertTrue(df1.equals(df3))
1818 # Check reading the columns.
1819 columns = df2.columns
1820 columns2 = self.butler.get(
1821 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1822 )
1823 self.assertTrue(columns2.equals(columns))
1825 # Check reading the schema.
1826 schema = DataFrameSchema(df1)
1827 schema2 = self.butler.get(
1828 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1829 )
1830 self.assertEqual(schema2, schema)
1832 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1833 def testWriteArrowTableReadAsAstropyTable(self):
1834 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
1836 self.butler.put(tab1, self.datasetType, dataId={})
1838 # Read back out as an astropy table.
1839 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1840 _checkAstropyTableEquality(tab1, tab2)
1842 # Read back out as an arrow table, convert to astropy table.
1843 atab3 = self.butler.get(self.datasetType, dataId={})
1844 tab3 = arrow_to_astropy(atab3)
1845 _checkAstropyTableEquality(tab1, tab3)
1847 # Check reading the columns.
1848 columns = list(tab2.columns.keys())
1849 columns2 = self.butler.get(
1850 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1851 )
1852 self.assertEqual(columns2, columns)
1854 # Check reading the schema.
1855 schema = ArrowAstropySchema(tab1)
1856 schema2 = self.butler.get(
1857 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema"
1858 )
1859 self.assertEqual(schema2, schema)
1861 # Check the schema conversions and units.
1862 arrow_schema = schema.to_arrow_schema()
1863 for name in arrow_schema.names:
1864 field_metadata = arrow_schema.field(name).metadata
1865 if (
1866 b"description" in field_metadata
1867 and (description := field_metadata[b"description"].decode("UTF-8")) != ""
1868 ):
1869 self.assertEqual(schema2.schema[name].description, description)
1870 else:
1871 self.assertIsNone(schema2.schema[name].description)
1872 if b"unit" in field_metadata and (unit := field_metadata[b"unit"].decode("UTF-8")) != "":
1873 self.assertEqual(schema2.schema[name].unit, units.Unit(unit))
1875 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1876 def testWriteArrowTableReadAsNumpyTable(self):
1877 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1879 self.butler.put(tab1, self.datasetType, dataId={})
1881 # Read back out as a numpy table.
1882 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
1883 _checkNumpyTableEquality(tab1, tab2)
1885 # Read back out as an arrow table, convert to numpy table.
1886 atab3 = self.butler.get(self.datasetType, dataId={})
1887 tab3 = arrow_to_numpy(atab3)
1888 _checkNumpyTableEquality(tab1, tab3)
1890 # Check reading the columns.
1891 columns = list(tab2.dtype.names)
1892 columns2 = self.butler.get(
1893 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1894 )
1895 self.assertEqual(columns2, columns)
1897 # Check reading the schema.
1898 schema = ArrowNumpySchema(tab1.dtype)
1899 schema2 = self.butler.get(
1900 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema"
1901 )
1902 self.assertEqual(schema2, schema)
1904 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1905 def testWriteArrowTableReadAsNumpyDict(self):
1906 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1908 self.butler.put(tab1, self.datasetType, dataId={})
1910 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
1911 tab2_numpy = _numpy_dict_to_numpy(tab2)
1912 _checkNumpyTableEquality(tab1, tab2_numpy)
1914 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1915 def testWriteReadAstropyTableLossless(self):
1916 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
1918 self.butler.put(tab1, self.datasetType, dataId={})
1920 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1922 _checkAstropyTableEquality(tab1, tab2)
1925@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with ArroWTable without pyarrow.")
1926class InMemoryArrowTableDelegateTestCase(ParquetFormatterArrowTableTestCase):
1927 """Tests for InMemoryDatastore, using ArrowTableDelegate."""
1929 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
1931 def testBadInput(self):
1932 tab1 = _makeSimpleArrowTable()
1933 delegate = ArrowTableDelegate("ArrowTable")
1935 with self.assertRaises(ValueError):
1936 delegate.handleParameters(inMemoryDataset="not_an_arrow_table")
1938 with self.assertRaises(NotImplementedError):
1939 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]})
1941 with self.assertRaises(AttributeError):
1942 delegate.getComponent(composite=tab1, componentName="nothing")
1944 def testStorageClass(self):
1945 tab1 = _makeSimpleArrowTable()
1947 factory = StorageClassFactory()
1948 factory.addFromConfig(StorageClassConfig())
1950 storageClass = factory.findStorageClass(type(tab1), compare_types=False)
1951 # Force the name lookup to do name matching.
1952 storageClass._pytype = None
1953 self.assertEqual(storageClass.name, "ArrowTable")
1955 storageClass = factory.findStorageClass(type(tab1), compare_types=True)
1956 # Force the name lookup to do name matching.
1957 storageClass._pytype = None
1958 self.assertEqual(storageClass.name, "ArrowTable")
1961@unittest.skipUnless(np is not None, "Cannot test ParquetFormatterArrowNumpy without numpy.")
1962@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowNumpy without pyarrow.")
1963class ParquetFormatterArrowNumpyDictTestCase(unittest.TestCase):
1964 """Tests for ParquetFormatter, ArrowNumpyDict, using local file store."""
1966 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
1968 def setUp(self):
1969 """Create a new butler root for each test."""
1970 self.root = makeTestTempDir(TESTDIR)
1971 config = Config(self.configFile)
1972 self.butler = Butler.from_config(
1973 make_repo_for_test(self.root, config=config), writeable=True, run="test_run"
1974 )
1975 self.enterContext(self.butler)
1976 # No dimensions in dataset type so we don't have to worry about
1977 # inserting dimension data or defining data IDs.
1978 self.datasetType = DatasetType(
1979 "data", dimensions=(), storageClass="ArrowNumpyDict", universe=self.butler.dimensions
1980 )
1981 self.butler.registry.registerDatasetType(self.datasetType)
1983 def tearDown(self):
1984 removeTestTempDir(self.root)
1986 def testNumpyDict(self):
1987 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1988 dict1 = _numpy_to_numpy_dict(tab1)
1990 self.butler.put(dict1, self.datasetType, dataId={})
1991 # Read the whole table.
1992 dict2 = self.butler.get(self.datasetType, dataId={})
1993 _checkNumpyDictEquality(dict1, dict2)
1994 # Read the columns.
1995 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1996 self.assertEqual(len(columns2), len(dict1.keys()))
1997 for name in dict1:
1998 self.assertIn(name, columns2)
1999 # Read the rowcount.
2000 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
2001 self.assertEqual(rowcount, len(dict1["a"]))
2002 # Read the schema.
2003 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
2004 self.assertEqual(schema, ArrowNumpySchema(tab1.dtype))
2005 # Read just some columns a few different ways.
2006 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
2007 subdict = {key: dict1[key] for key in ["a", "c"]}
2008 _checkNumpyDictEquality(subdict, tab3)
2009 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
2010 subdict = {key: dict1[key] for key in ["a"]}
2011 _checkNumpyDictEquality(subdict, tab4)
2012 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
2013 subdict = {key: dict1[key] for key in ["index", "a"]}
2014 _checkNumpyDictEquality(subdict, tab5)
2015 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
2016 subdict = {key: dict1[key] for key in ["ddd"]}
2017 _checkNumpyDictEquality(subdict, tab6)
2018 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
2019 subdict = {key: dict1[key] for key in ["a"]}
2020 _checkNumpyDictEquality(subdict, tab7)
2021 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]})
2022 subdict = {key: dict1[key] for key in ["ddd", "dtn", "dtu", "a"]}
2023 _checkNumpyDictEquality(subdict, tab8)
2024 # Passing an unrecognized column should be a ValueError.
2025 with self.assertRaises(ValueError):
2026 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
2028 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
2029 def testWriteNumpyDictReadAsArrowTable(self):
2030 tab1 = _makeSimpleNumpyTable(include_multidim=True)
2031 dict1 = _numpy_to_numpy_dict(tab1)
2033 self.butler.put(dict1, self.datasetType, dataId={})
2035 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
2037 tab2_dict = arrow_to_numpy_dict(tab2)
2039 _checkNumpyDictEquality(dict1, tab2_dict)
2041 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
2042 def testWriteNumpyDictReadAsDataFrame(self):
2043 tab1 = _makeSimpleNumpyTable()
2044 dict1 = _numpy_to_numpy_dict(tab1)
2046 self.butler.put(dict1, self.datasetType, dataId={})
2048 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
2050 # The order of the dict may get mixed up, so we need to check column
2051 # by column. We also need to do this in dataframe form because pandas
2052 # changes the datatype of the string column.
2053 tab1_df = pd.DataFrame(tab1)
2055 self.assertEqual(set(tab1_df.columns), set(tab2.columns))
2056 for col in tab1_df.columns:
2057 self.assertTrue(np.all(tab1_df[col].values == tab2[col].values))
2059 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
2060 def testWriteNumpyDictReadAsAstropyTable(self):
2061 tab1 = _makeSimpleNumpyTable(include_multidim=True)
2062 dict1 = _numpy_to_numpy_dict(tab1)
2064 self.butler.put(dict1, self.datasetType, dataId={})
2066 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
2067 tab2_dict = _astropy_to_numpy_dict(tab2)
2069 _checkNumpyDictEquality(dict1, tab2_dict)
2071 def testWriteNumpyDictReadAsNumpyTable(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="ArrowNumpy")
2078 tab2_dict = _numpy_to_numpy_dict(tab2)
2080 _checkNumpyDictEquality(dict1, tab2_dict)
2082 def testWriteNumpyDictBad(self):
2083 dict1 = {"a": 4, "b": np.ndarray([1])}
2084 with self.assertRaises(RuntimeError):
2085 self.butler.put(dict1, self.datasetType, dataId={})
2087 dict2 = {"a": np.zeros(4), "b": np.zeros(5)}
2088 with self.assertRaises(RuntimeError):
2089 self.butler.put(dict2, self.datasetType, dataId={})
2091 dict3 = {"a": [0] * 5, "b": np.zeros(5)}
2092 with self.assertRaises(RuntimeError):
2093 self.butler.put(dict3, self.datasetType, dataId={})
2095 dict4 = {"a": np.zeros(4), "b": np.zeros(4, dtype="O")}
2096 with self.assertRaises(RuntimeError):
2097 self.butler.put(dict4, self.datasetType, dataId={})
2099 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
2100 def testWriteReadAstropyTableLossless(self):
2101 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
2103 self.butler.put(tab1, self.datasetType, dataId={})
2105 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
2107 _checkAstropyTableEquality(tab1, tab2)
2110@unittest.skipUnless(np is not None, "Cannot test InMemoryDatastore with NumpyDict without numpy.")
2111@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with NumpyDict without pyarrow.")
2112class InMemoryNumpyDictDelegateTestCase(ParquetFormatterArrowNumpyDictTestCase):
2113 """Tests for InMemoryDatastore, using ArrowTableDelegate with
2114 Numpy dict.
2115 """
2117 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
2119 def testWriteNumpyDictBad(self):
2120 # The sub-type checking is not done on in-memory datastore.
2121 pass
2124@unittest.skipUnless(pa is not None, "Cannot test ArrowSchema without pyarrow.")
2125class ParquetFormatterArrowSchemaTestCase(unittest.TestCase):
2126 """Tests for ParquetFormatter, ArrowSchema, using local file datastore."""
2128 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
2130 def setUp(self):
2131 """Create a new butler root for each test."""
2132 self.root = makeTestTempDir(TESTDIR)
2133 config = Config(self.configFile)
2134 self.butler = Butler.from_config(
2135 make_repo_for_test(self.root, config=config), writeable=True, run="test_run"
2136 )
2137 self.enterContext(self.butler)
2138 # No dimensions in dataset type so we don't have to worry about
2139 # inserting dimension data or defining data IDs.
2140 self.datasetType = DatasetType(
2141 "data", dimensions=(), storageClass="ArrowSchema", universe=self.butler.dimensions
2142 )
2143 self.butler.registry.registerDatasetType(self.datasetType)
2145 def tearDown(self):
2146 removeTestTempDir(self.root)
2148 def _makeTestSchema(self):
2149 schema = pa.schema(
2150 [
2151 pa.field(
2152 "int32",
2153 pa.int32(),
2154 nullable=False,
2155 metadata={
2156 "description": "32-bit integer",
2157 "unit": "",
2158 },
2159 ),
2160 pa.field(
2161 "int64",
2162 pa.int64(),
2163 nullable=False,
2164 metadata={
2165 "description": "64-bit integer",
2166 "unit": "",
2167 },
2168 ),
2169 pa.field(
2170 "uint64",
2171 pa.uint64(),
2172 nullable=False,
2173 metadata={
2174 "description": "64-bit unsigned integer",
2175 "unit": "",
2176 },
2177 ),
2178 pa.field(
2179 "float32",
2180 pa.float32(),
2181 nullable=False,
2182 metadata={
2183 "description": "32-bit float",
2184 "unit": "count",
2185 },
2186 ),
2187 pa.field(
2188 "float64",
2189 pa.float64(),
2190 nullable=False,
2191 metadata={
2192 "description": "64-bit float",
2193 "unit": "nJy",
2194 },
2195 ),
2196 pa.field(
2197 "fixed_size_list",
2198 pa.list_(pa.float64(), list_size=10),
2199 nullable=False,
2200 metadata={
2201 "description": "Fixed size list of 64-bit floats.",
2202 "unit": "nJy",
2203 },
2204 ),
2205 pa.field(
2206 "variable_size_list",
2207 pa.list_(pa.float64()),
2208 nullable=False,
2209 metadata={
2210 "description": "Variable size list of 64-bit floats.",
2211 "unit": "nJy",
2212 },
2213 ),
2214 # One of these fields will have no description.
2215 pa.field(
2216 "string",
2217 pa.string(),
2218 nullable=False,
2219 metadata={
2220 "unit": "",
2221 },
2222 ),
2223 # One of these fields will have no metadata.
2224 pa.field(
2225 "binary",
2226 pa.binary(),
2227 nullable=False,
2228 ),
2229 ]
2230 )
2232 return schema
2234 def testArrowSchema(self):
2235 schema1 = self._makeTestSchema()
2236 self.butler.put(schema1, self.datasetType, dataId={})
2238 schema2 = self.butler.get(self.datasetType, dataId={})
2239 self.assertEqual(schema2, schema1)
2241 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe schema without pandas.")
2242 def testWriteArrowSchemaReadAsDataFrameSchema(self):
2243 schema1 = self._makeTestSchema()
2244 self.butler.put(schema1, self.datasetType, dataId={})
2246 df_schema1 = DataFrameSchema.from_arrow(schema1)
2248 df_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrameSchema")
2249 self.assertEqual(df_schema2, df_schema1)
2251 @unittest.skipUnless(atable is not None, "Cannot test reading as an astropy schema without astropy.")
2252 def testWriteArrowSchemaReadAsArrowAstropySchema(self):
2253 schema1 = self._makeTestSchema()
2254 self.butler.put(schema1, self.datasetType, dataId={})
2256 ap_schema1 = ArrowAstropySchema.from_arrow(schema1)
2258 ap_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropySchema")
2259 self.assertEqual(ap_schema2, ap_schema1)
2261 # Confirm that the ap_schema2 has the unit/description we expect.
2262 for name in schema1.names:
2263 field_metadata = schema1.field(name).metadata
2264 if field_metadata is None:
2265 continue
2266 if (
2267 b"description" in field_metadata
2268 and (description := field_metadata[b"description"].decode("UTF-8")) != ""
2269 ):
2270 self.assertEqual(ap_schema2.schema[name].description, description)
2271 else:
2272 self.assertIsNone(ap_schema2.schema[name].description)
2273 if b"unit" in field_metadata and (unit := field_metadata[b"unit"].decode("UTF-8")) != "":
2274 self.assertEqual(ap_schema2.schema[name].unit, units.Unit(unit))
2276 @unittest.skipUnless(atable is not None, "Cannot test reading as an numpy schema without numpy.")
2277 def testWriteArrowSchemaReadAsArrowNumpySchema(self):
2278 schema1 = self._makeTestSchema()
2279 self.butler.put(schema1, self.datasetType, dataId={})
2281 np_schema1 = ArrowNumpySchema.from_arrow(schema1)
2283 np_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpySchema")
2284 self.assertEqual(np_schema2, np_schema1)
2287@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with ArrowSchema without pyarrow.")
2288class InMemoryArrowSchemaDelegateTestCase(ParquetFormatterArrowSchemaTestCase):
2289 """Tests for InMemoryDatastore and ArrowSchema."""
2291 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
2294@unittest.skipUnless(pa is not None, "Cannot test remote datastore without pyarrow.")
2295@unittest.skipUnless(fsspec is not None, "Cannot test remote datastore without fsspec.")
2296class ParquetFormatterArrowTableRemoteTestCase(unittest.TestCase):
2297 """Tests for arrow table/parquet with a datastore that reports itself as
2298 not local.
2299 """
2301 # Code is adapted from test_butler.py
2302 configFile = os.path.join(TESTDIR, "config/basic/butler-remotetest-store.yaml")
2304 def setUp(self):
2305 config = Config(self.configFile)
2307 self.root = makeTestTempDir(TESTDIR)
2308 # The space in the directory name is deliberate, to exercise URI
2309 # percent-encoding.
2310 root_path = os.path.join(self.root, "butler root")
2311 os.makedirs(root_path)
2312 rooturi = make_remote_test_uri(root_path)
2313 config.update({"datastore": {"datastore": {"root": str(rooturi)}}})
2315 # The registry database has to live on a real local file system.
2316 self.reg_dir = makeTestTempDir(TESTDIR)
2317 config["registry", "db"] = f"sqlite:///{self.reg_dir}/gen3.sqlite3"
2319 make_repo_for_test(rooturi, config=config, forceConfigRoot=False)
2320 self.tmpConfigFile = str(rooturi.join("butler.yaml", forceDirectory=False))
2322 self.butler = Butler(self.tmpConfigFile, writeable=True, run="test_run")
2323 self.enterContext(self.butler)
2325 # No dimensions in dataset type so we don't have to worry about
2326 # inserting dimension data or defining data IDs.
2327 self.datasetType = DatasetType(
2328 "data", dimensions=(), storageClass="ArrowTable", universe=self.butler.dimensions
2329 )
2330 self.butler.registry.registerDatasetType(self.datasetType)
2332 def tearDown(self):
2333 removeTestTempDir(self.reg_dir)
2334 removeTestTempDir(self.root)
2336 def testArrowTableRemote(self):
2337 tab1 = _makeSimpleArrowTable(include_multidim=True, include_masked=True)
2339 self.butler.put(tab1, self.datasetType, dataId={})
2341 # Read the whole Table.
2342 tab2 = self.butler.get(self.datasetType, dataId={})
2343 # We convert to use the numpy testing framework to handle nan
2344 # comparisons.
2345 self.assertEqual(tab1.schema, tab2.schema)
2346 tab1_np = arrow_to_numpy(tab1)
2347 tab2_np = arrow_to_numpy(tab2)
2348 for col in tab1.column_names:
2349 np.testing.assert_array_equal(tab2_np[col], tab1_np[col])
2350 # Read the columns.
2351 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
2352 self.assertEqual(len(columns2), len(tab1.schema.names))
2353 for i, name in enumerate(tab1.schema.names):
2354 self.assertEqual(columns2[i], name)
2355 # Read the rowcount.
2356 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
2357 self.assertEqual(rowcount, len(tab1))
2358 # Read the schema.
2359 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
2360 self.assertEqual(schema, tab1.schema)
2361 # Read just some columns a few different ways.
2362 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
2363 self.assertEqual(tab3, tab1.select(("a", "c")))
2364 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
2365 self.assertEqual(tab4, tab1.select(("a",)))
2366 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
2367 self.assertEqual(tab5, tab1.select(("index", "a")))
2368 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
2369 self.assertEqual(tab6, tab1.select(("ddd",)))
2370 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
2371 self.assertEqual(tab7, tab1.select(("a",)))
2372 # Passing an unrecognized column should be a ValueError.
2373 with self.assertRaises(ValueError):
2374 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
2377@unittest.skipUnless(np is not None, "Cannot test compute_row_group_size without numpy.")
2378@unittest.skipUnless(pa is not None, "Cannot test compute_row_group_size without pyarrow.")
2379class ComputeRowGroupSizeTestCase(unittest.TestCase):
2380 """Tests for compute_row_group_size."""
2382 def testRowGroupSizeNoMetadata(self):
2383 numpyTable = _makeSimpleNumpyTable(include_multidim=True)
2385 # We can't use the numpy_to_arrow convenience function because
2386 # that adds metadata.
2387 type_list = _numpy_dtype_to_arrow_types(numpyTable.dtype)
2388 schema = pa.schema(type_list)
2389 arrays = _numpy_style_arrays_to_arrow_arrays(
2390 numpyTable.dtype,
2391 len(numpyTable),
2392 numpyTable,
2393 schema,
2394 )
2395 arrowTable = pa.Table.from_arrays(arrays, schema=schema)
2397 row_group_size = compute_row_group_size(arrowTable.schema)
2399 self.assertGreater(row_group_size, 1_000_000)
2400 self.assertLess(row_group_size, 2_000_000)
2402 def testRowGroupSizeWithMetadata(self):
2403 numpyTable = _makeSimpleNumpyTable(include_multidim=True)
2405 arrowTable = numpy_to_arrow(numpyTable)
2407 row_group_size = compute_row_group_size(arrowTable.schema)
2409 self.assertGreater(row_group_size, 1_000_000)
2410 self.assertLess(row_group_size, 2_000_000)
2412 def testRowGroupSizeTinyTable(self):
2413 numpyTable = np.zeros(1, dtype=[("a", np.bool_)])
2415 arrowTable = numpy_to_arrow(numpyTable)
2417 row_group_size = compute_row_group_size(arrowTable.schema)
2419 self.assertGreater(row_group_size, 1_000_000)
2421 @unittest.skipUnless(pd is not None, "Cannot run testRowGroupSizeDataFrameWithLists without pandas.")
2422 def testRowGroupSizeDataFrameWithLists(self):
2423 df = pd.DataFrame({"a": np.zeros(10), "b": [[0, 0]] * 10, "c": [[0.0, 0.0]] * 10, "d": [[]] * 10})
2424 arrowTable = pandas_to_arrow(df)
2425 row_group_size = compute_row_group_size(arrowTable.schema)
2427 self.assertGreater(row_group_size, 1_000_000)
2430def _checkAstropyTableEquality(table1, table2, skip_units=False, has_bigendian=False):
2431 """Check if two astropy tables have the same columns/values.
2433 Parameters
2434 ----------
2435 table1 : `astropy.table.Table`
2436 table2 : `astropy.table.Table`
2437 skip_units : `bool`
2438 has_bigendian : `bool`
2439 """
2440 if not has_bigendian:
2441 assert table1.dtype == table2.dtype
2442 else:
2443 for name in table1.dtype.names:
2444 # Only check type matches, force to little-endian.
2445 assert table1.dtype[name].newbyteorder(">") == table2.dtype[name].newbyteorder(">")
2447 # Strip provenance before comparison.
2448 DatasetProvenance.strip_provenance_from_flat_dict(table1.meta)
2449 DatasetProvenance.strip_provenance_from_flat_dict(table2.meta)
2450 assert table1.meta == table2.meta
2451 if not skip_units:
2452 for name in table1.columns:
2453 assert table1[name].unit == table2[name].unit
2454 assert table1[name].description == table2[name].description
2455 assert table1[name].format == table2[name].format
2457 for name in table1.columns:
2458 # We need to check masked/regular columns after filling.
2459 has_masked = False
2460 if isinstance(table1[name], atable.column.MaskedColumn):
2461 c1 = table1[name].filled()
2462 has_masked = True
2463 else:
2464 c1 = np.array(table1[name])
2465 if has_masked:
2466 assert isinstance(table2[name], atable.column.MaskedColumn)
2467 c2 = table2[name].filled()
2468 else:
2469 assert not isinstance(table2[name], atable.column.MaskedColumn)
2470 c2 = np.array(table2[name])
2471 np.testing.assert_array_equal(c1, c2)
2472 # If we have a masked column then we test the underlying data.
2473 if has_masked:
2474 np.testing.assert_array_equal(np.array(c1), np.array(c2))
2475 np.testing.assert_array_equal(table1[name].mask, table2[name].mask)
2478def _checkNumpyTableEquality(table1, table2, has_bigendian=False):
2479 """Check if two numpy tables have the same columns/values
2481 Parameters
2482 ----------
2483 table1 : `numpy.ndarray`
2484 table2 : `numpy.ndarray`
2485 has_bigendian : `bool`
2486 """
2487 assert table1.dtype.names == table2.dtype.names
2488 for name in table1.dtype.names:
2489 if not has_bigendian:
2490 assert table1.dtype[name] == table2.dtype[name]
2491 else:
2492 # Only check type matches, force to little-endian.
2493 assert table1.dtype[name].newbyteorder(">") == table2.dtype[name].newbyteorder(">")
2494 assert np.all(table1 == table2)
2497def _checkNumpyDictEquality(dict1, dict2):
2498 """Check if two numpy dicts have the same columns/values.
2500 Parameters
2501 ----------
2502 dict1 : `dict` [`str`, `np.ndarray`]
2503 dict2 : `dict` [`str`, `np.ndarray`]
2504 """
2505 assert set(dict1.keys()) == set(dict2.keys())
2506 for name in dict1:
2507 assert dict1[name].dtype == dict2[name].dtype
2508 assert np.all(dict1[name] == dict2[name])
2511if __name__ == "__main__":
2512 unittest.main()