Coverage for tests/test_parquet.py: 99%
1306 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-17 13:47 -0700
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-17 13:47 -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.utils import makeTestTempDir, removeTestTempDir
110from lsst.resources.tests import make_remote_test_uri
112TESTDIR = os.path.abspath(os.path.dirname(__file__))
115def _makeSimpleNumpyTable(include_multidim=False, include_bigendian=False):
116 """Make a simple numpy table with random data.
118 Parameters
119 ----------
120 include_multidim : `bool`
121 Include multi-dimensional columns.
122 include_bigendian : `bool`
123 Include big-endian columns.
125 Returns
126 -------
127 numpyTable : `numpy.ndarray`
128 """
129 nrow = 5
131 dtype = [
132 ("index", "i4"),
133 ("a", "f8"),
134 ("b", "f8"),
135 ("c", "f8"),
136 ("ddd", "f8"),
137 ("f", "i8"),
138 ("strcol", "U10"),
139 ("bytecol", "S10"),
140 ("dtn", "datetime64[ns]"),
141 ("dtu", "datetime64[us]"),
142 ]
144 if include_multidim:
145 dtype.extend(
146 [
147 ("d1", "f4", (5,)),
148 ("d2", "i8", (5, 10)),
149 ("d3", "f8", (5, 10)),
150 ]
151 )
153 if include_bigendian:
154 dtype.extend([("a_bigendian", ">f8"), ("f_bigendian", ">i8")])
156 data = np.zeros(nrow, dtype=dtype)
157 data["index"][:] = np.arange(nrow)
158 data["a"] = np.random.randn(nrow)
159 data["b"] = np.random.randn(nrow)
160 data["c"] = np.random.randn(nrow)
161 data["ddd"] = np.random.randn(nrow)
162 data["f"] = np.arange(nrow) * 10
163 data["strcol"][:] = "teststring"
164 data["bytecol"][:] = "teststring"
165 data["dtn"] = datetime.datetime.fromisoformat("2024-07-23")
166 data["dtu"] = datetime.datetime.fromisoformat("2024-07-23")
168 if include_multidim:
169 data["d1"] = np.random.randn(data["d1"].size).reshape(data["d1"].shape)
170 data["d2"] = np.arange(data["d2"].size).reshape(data["d2"].shape)
171 data["d3"] = np.asfortranarray(np.random.randn(data["d3"].size).reshape(data["d3"].shape))
173 if include_bigendian:
174 data["a_bigendian"][:] = data["a"]
175 data["f_bigendian"][:] = data["f"]
177 return data
180def _makeSingleIndexDataFrame(include_masked=False, include_lists=False):
181 """Make a single index data frame for testing.
183 Parameters
184 ----------
185 include_masked : `bool`
186 Include masked columns.
187 include_lists : `bool`
188 Include list columns.
190 Returns
191 -------
192 dataFrame : `~pandas.DataFrame`
193 The test dataframe.
194 allColumns : `list` [`str`]
195 List of all the columns (including index columns).
196 """
197 data = _makeSimpleNumpyTable()
198 df = pd.DataFrame(data)
199 df = df.set_index("index")
201 if include_masked:
202 nrow = len(df)
204 df["m1"] = pd.array(np.arange(nrow), dtype=pd.Int64Dtype())
205 df["m2"] = pd.array(np.arange(nrow), dtype=np.float32)
206 df["mstrcol"] = pd.array(np.array(["text"] * nrow))
207 df.loc[1, ["m1", "m2", "mstrcol"]] = None
208 df.loc[0, "m1"] = 1649900760361600113
210 if include_lists:
211 nrow = len(df)
213 df["l1"] = [[0, 0]] * nrow
214 df["l2"] = [[0.0, 0.0]] * nrow
215 df["l3"] = [[]] * nrow
217 allColumns = df.columns.append(pd.Index(df.index.names))
219 return df, allColumns
222def _makeMultiIndexDataFrame():
223 """Make a multi-index data frame for testing.
225 Returns
226 -------
227 dataFrame : `~pandas.DataFrame`
228 The test dataframe.
229 """
230 columns = pd.MultiIndex.from_tuples(
231 [
232 ("g", "a"),
233 ("g", "b"),
234 ("g", "c"),
235 ("r", "a"),
236 ("r", "b"),
237 ("r", "c"),
238 ],
239 names=["filter", "column"],
240 )
241 df = pd.DataFrame(np.random.randn(5, 6), index=np.arange(5, dtype=int), columns=columns)
243 return df
246def _makeSimpleAstropyTable(include_multidim=False, include_masked=False, include_bigendian=False):
247 """Make an astropy table for testing.
249 Parameters
250 ----------
251 include_multidim : `bool`
252 Include multi-dimensional columns.
253 include_masked : `bool`
254 Include masked columns.
255 include_bigendian : `bool`
256 Include big-endian columns.
258 Returns
259 -------
260 astropyTable : `astropy.table.Table`
261 The test table.
262 """
263 data = _makeSimpleNumpyTable(include_multidim=include_multidim, include_bigendian=include_bigendian)
264 # Add a couple of units.
265 table = atable.Table(data)
266 table["a"].unit = units.degree
267 table["a"].description = "Description of column a"
268 table["b"].unit = units.meter
269 table["b"].description = "Description of column b"
271 # Add some masked columns.
272 if include_masked:
273 nrow = len(table)
274 mask = np.zeros(nrow, dtype=bool)
275 mask[1] = True
276 # We set the masked columns with the underlying sentinel value
277 # to be able test after serialization.
279 # Masked 64-bit integer.
280 arr = np.arange(nrow, dtype="i8")
281 arr[mask] = -1
282 arr[0] = 1649900760361600113
283 table["m_i8"] = np.ma.masked_array(data=arr, mask=mask, fill_value=-1)
284 # Masked 32-bit float.
285 arr = np.arange(nrow, dtype="f4")
286 arr[mask] = np.nan
287 table["m_f4"] = np.ma.masked_array(data=arr, mask=mask, fill_value=np.nan)
288 # Unmasked 32-bit float with NaNs.
289 table["um_f4"] = arr
290 # Masked 64-bit float.
291 arr = np.arange(nrow, dtype="f8")
292 arr[mask] = np.nan
293 table["m_f8"] = np.ma.masked_array(data=arr, mask=mask, fill_value=np.nan)
294 # Unmasked 64-bit float with NaNs.
295 table["um_f8"] = arr
296 # Masked boolean.
297 arr = np.zeros(nrow, dtype=np.bool_)
298 arr[mask] = True
299 table["m_bool"] = np.ma.masked_array(data=arr, mask=mask, fill_value=True)
300 # Masked unsigned 32-bit unsigned int.
301 arr = np.arange(nrow, dtype="u4")
302 arr[mask] = 0
303 table["m_u4"] = np.ma.masked_array(data=arr, mask=mask, fill_value=0)
304 # Masked string.
305 table["m_str"] = np.ma.masked_array(data=np.array(["text"] * nrow), mask=mask, fill_value="")
306 # Masked bytes.
307 table["m_byte"] = np.ma.masked_array(data=np.array([b"bytes"] * nrow), mask=mask, fill_value=b"")
309 return table
312def _makeSimpleArrowTable(include_multidim=False, include_masked=False):
313 """Make an arrow table for testing.
315 Parameters
316 ----------
317 include_multidim : `bool`
318 Include multi-dimensional columns.
319 include_masked : `bool`
320 Include masked columns.
322 Returns
323 -------
324 arrowTable : `pyarrow.Table`
325 The test table.
326 """
327 data = _makeSimpleAstropyTable(include_multidim=include_multidim, include_masked=include_masked)
328 return astropy_to_arrow(data)
331@unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.")
332@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterDataFrame without pyarrow.")
333class ParquetFormatterDataFrameTestCase(unittest.TestCase):
334 """Tests for ParquetFormatter, DataFrame, using local file datastore."""
336 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
338 def setUp(self):
339 """Create a new butler root for each test."""
340 self.root = makeTestTempDir(TESTDIR)
341 config = Config(self.configFile)
342 self.run = "test_run"
343 self.butler = Butler.from_config(
344 Butler.makeRepo(self.root, config=config), writeable=True, run=self.run
345 )
346 self.enterContext(self.butler)
347 # No dimensions in dataset type so we don't have to worry about
348 # inserting dimension data or defining data IDs.
349 self.datasetType = DatasetType(
350 "data", dimensions=(), storageClass="DataFrame", universe=self.butler.dimensions
351 )
352 self.butler.registry.registerDatasetType(self.datasetType)
354 def tearDown(self):
355 removeTestTempDir(self.root)
357 def testSingleIndexDataFrame(self):
358 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True)
360 self.butler.put(df1, self.datasetType, dataId={})
361 # Read the whole DataFrame.
362 df2 = self.butler.get(self.datasetType, dataId={})
363 self.assertTrue(df1.equals(df2))
364 # Read just the column descriptions.
365 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
366 self.assertTrue(allColumns.equals(columns2))
367 # Read the rowcount.
368 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
369 self.assertEqual(rowcount, len(df1))
370 # Read the schema.
371 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
372 self.assertEqual(schema, DataFrameSchema(df1))
373 # Read just some columns a few different ways.
374 df3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
375 self.assertTrue(df1.loc[:, ["a", "c"]].equals(df3))
376 df4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
377 self.assertTrue(df1.loc[:, ["a"]].equals(df4))
378 df5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
379 self.assertTrue(df1.loc[:, ["a"]].equals(df5))
380 df6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
381 self.assertTrue(df1.loc[:, ["ddd"]].equals(df6))
382 df7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
383 self.assertTrue(df1.loc[:, ["a"]].equals(df7))
384 df8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d*"]})
385 self.assertTrue(df1.loc[:, ["ddd", "dtn", "dtu"]].equals(df8))
386 df9 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d*", "d*"]})
387 self.assertTrue(df1.loc[:, ["ddd", "dtn", "dtu"]].equals(df9))
388 # Passing an unrecognized column should be a ValueError.
389 with self.assertRaises(ValueError):
390 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
392 def testSingleIndexDataFrameWithLists(self):
393 df1, allColumns = _makeSingleIndexDataFrame(include_lists=True)
395 self.butler.put(df1, self.datasetType, dataId={})
396 # Read the whole DataFrame.
397 df2 = self.butler.get(self.datasetType, dataId={})
399 # We need to check the list columns specially because they go
400 # from lists to arrays.
401 for col in ["l1", "l2", "l3"]:
402 for i in range(len(df1)):
403 self.assertTrue(np.all(df2[col].values[i] == df1[col].values[i]))
405 def testMultiIndexDataFrame(self):
406 df1 = _makeMultiIndexDataFrame()
408 self.butler.put(df1, self.datasetType, dataId={})
409 # Read the whole DataFrame.
410 df2 = self.butler.get(self.datasetType, dataId={})
411 self.assertTrue(df1.equals(df2))
412 # Read just the column descriptions.
413 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
414 self.assertTrue(df1.columns.equals(columns2))
415 self.assertEqual(columns2.names, df1.columns.names)
416 # Read the rowcount.
417 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
418 self.assertEqual(rowcount, len(df1))
419 # Read the schema.
420 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
421 self.assertEqual(schema, DataFrameSchema(df1))
422 # Read just some columns a few different ways.
423 df3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": {"filter": "g"}})
424 self.assertTrue(df1.loc[:, ["g"]].equals(df3))
425 df4 = self.butler.get(
426 self.datasetType, dataId={}, parameters={"columns": {"filter": ["r"], "column": "a"}}
427 )
428 self.assertTrue(df1.loc[:, [("r", "a")]].equals(df4))
429 column_list = [("g", "a"), ("r", "c")]
430 df5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": column_list})
431 self.assertTrue(df1.loc[:, column_list].equals(df5))
432 column_dict = {"filter": "r", "column": ["a", "b"]}
433 df6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": column_dict})
434 self.assertTrue(df1.loc[:, [("r", "a"), ("r", "b")]].equals(df6))
435 # Passing an unrecognized column should be a ValueError.
436 with self.assertRaises(ValueError):
437 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d"]})
439 def testSingleIndexDataFrameEmptyString(self):
440 """Test persisting a single index dataframe with empty strings."""
441 df1, _ = _makeSingleIndexDataFrame()
443 # Set one of the strings to None
444 df1.at[1, "strcol"] = None
446 self.butler.put(df1, self.datasetType, dataId={})
447 # Read the whole DataFrame.
448 df2 = self.butler.get(self.datasetType, dataId={})
449 self.assertTrue(df1.equals(df2))
451 def testSingleIndexDataFrameAllEmptyStrings(self):
452 """Test persisting a single index dataframe with an empty string
453 column.
454 """
455 df1, _ = _makeSingleIndexDataFrame()
457 # Set all of the strings to None
458 df1.loc[0:, "strcol"] = None
460 self.butler.put(df1, self.datasetType, dataId={})
461 # Read the whole DataFrame.
462 df2 = self.butler.get(self.datasetType, dataId={})
463 self.assertTrue(df1.equals(df2))
465 def testLegacyDataFrame(self):
466 """Test writing a dataframe to parquet via pandas (without additional
467 metadata) and ensure that we can read it back with all the new
468 functionality.
469 """
470 df1, allColumns = _makeSingleIndexDataFrame()
472 if isinstance(df1.index, pd.RangeIndex): 472 ↛ 478line 472 didn't jump to line 478 because the condition on line 472 was never true
473 # Turn the RangeIndex into a regular index or it won't
474 # give us all the column names. This is necessary for pandas v3.
475 # Unfortunately, parquet files serialized directly with
476 # pandas v3 will not report their index column names if
477 # they are sequential integers.
478 df1.index = pd.Index(df1.index.to_numpy(), name=df1.index.name)
480 fname = os.path.join(self.root, "test_dataframe.parq")
481 df1.to_parquet(fname)
483 legacy_type = DatasetType(
484 "legacy_dataframe",
485 dimensions=(),
486 storageClass="DataFrame",
487 universe=self.butler.dimensions,
488 )
489 self.butler.registry.registerDatasetType(legacy_type)
491 data_id = {}
492 ref = DatasetRef(legacy_type, data_id, run=self.run)
493 dataset = FileDataset(path=fname, refs=[ref], formatter=ParquetFormatter)
495 self.butler.ingest(dataset, transfer="copy")
497 self.butler.put(df1, self.datasetType, dataId={})
499 df2a = self.butler.get(self.datasetType, dataId={})
500 df2b = self.butler.get("legacy_dataframe", dataId={})
501 self.assertTrue(df2a.equals(df2b))
503 df3a = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a"]})
504 df3b = self.butler.get("legacy_dataframe", dataId={}, parameters={"columns": ["a"]})
505 self.assertTrue(df3a.equals(df3b))
507 columns2a = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
508 columns2b = self.butler.get("legacy_dataframe.columns", dataId={})
509 self.assertTrue(columns2a.equals(columns2b))
511 rowcount2a = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
512 rowcount2b = self.butler.get("legacy_dataframe.rowcount", dataId={})
513 self.assertEqual(rowcount2a, rowcount2b)
515 schema2a = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
516 schema2b = self.butler.get("legacy_dataframe.schema", dataId={})
517 self.assertEqual(schema2a, schema2b)
519 def testDataFrameSchema(self):
520 tab1 = _makeSimpleArrowTable()
522 schema = DataFrameSchema.from_arrow(tab1.schema)
524 self.assertIsInstance(schema.schema, pd.DataFrame)
525 self.assertEqual(repr(schema), repr(schema._schema))
526 self.assertNotEqual(schema, "not_a_schema")
527 self.assertEqual(schema, schema)
529 tab2 = _makeMultiIndexDataFrame()
530 schema2 = DataFrameSchema(tab2)
532 self.assertNotEqual(schema, schema2)
534 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
535 def testWriteSingleIndexDataFrameReadAsAstropyTable(self):
536 df1, allColumns = _makeSingleIndexDataFrame()
538 self.butler.put(df1, self.datasetType, dataId={})
540 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
542 tab2_df = tab2.to_pandas(index="index")
543 self.assertTrue(df1.equals(tab2_df))
545 # Check reading the columns.
546 columns = list(tab2.columns.keys())
547 columns2 = self.butler.get(
548 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
549 )
550 # We check the set because pandas reorders the columns.
551 self.assertEqual(set(columns2), set(columns))
553 # Check reading the schema.
554 schema = ArrowAstropySchema(tab2)
555 schema2 = self.butler.get(
556 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema"
557 )
559 # The string types are objectified by pandas, and the order
560 # will be changed because of pandas indexing.
561 self.assertEqual(len(schema2.schema.columns), len(schema.schema.columns))
562 for name in schema.schema.columns:
563 self.assertIn(name, schema2.schema.columns)
564 if schema2.schema[name].dtype != np.dtype("O"):
565 self.assertEqual(schema2.schema[name].dtype, schema.schema[name].dtype)
567 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
568 def testWriteSingleIndexDataFrameWithMaskedColsReadAsAstropyTable(self):
569 # We need to special-case the write-as-pandas read-as-astropy code
570 # with masks because pandas has multiple ways to use masked columns.
571 # (The string column mask handling in particular is frustratingly
572 # inconsistent.)
573 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True)
575 self.butler.put(df1, self.datasetType, dataId={})
577 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
578 tab2_df = astropy_to_pandas(tab2, index="index")
580 self.assertTrue(df1.columns.equals(tab2_df.columns))
581 for name in tab2_df.columns:
582 col1 = df1[name]
583 col2 = tab2_df[name]
585 if col1.hasnans:
586 notNull = col1.notnull()
587 self.assertTrue(notNull.equals(col2.notnull()))
588 # Need to check value-by-value because column may
589 # be made of objects, depending on what pandas decides.
590 for index in notNull.values.nonzero()[0]:
591 self.assertEqual(col1[index], col2[index])
592 else:
593 self.assertTrue(col1.equals(col2))
595 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
596 def testWriteMultiIndexDataFrameReadAsAstropyTable(self):
597 df1 = _makeMultiIndexDataFrame()
599 self.butler.put(df1, self.datasetType, dataId={})
601 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
603 # This is an odd duck, it doesn't really round-trip.
604 # This test simply checks that it's readable, but definitely not
605 # recommended.
607 @unittest.skipUnless(atable is not None, "Cannot test writing as astropy without astropy.")
608 def testWriteAstropyTableWithMaskedColsReadAsSingleIndexDataFrame(self):
609 tab1 = _makeSimpleAstropyTable(include_masked=True)
611 self.butler.put(tab1, self.datasetType, dataId={})
613 tab2 = self.butler.get(self.datasetType, dataId={})
615 tab1_df = astropy_to_pandas(tab1)
616 self.assertTrue(tab1_df.equals(tab2))
618 tab2_astropy = pandas_to_astropy(tab2)
619 for col in tab1.dtype.names:
620 np.testing.assert_array_equal(tab2_astropy[col], tab1[col])
621 if isinstance(tab1[col], atable.column.MaskedColumn):
622 np.testing.assert_array_equal(tab2_astropy[col].mask, tab1[col].mask)
624 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
625 def testWriteSingleIndexDataFrameReadAsArrowTable(self):
626 df1, allColumns = _makeSingleIndexDataFrame()
628 self.butler.put(df1, self.datasetType, dataId={})
630 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
632 tab2_df = arrow_to_pandas(tab2)
633 self.assertTrue(df1.equals(tab2_df))
635 # Check reading the columns.
636 columns = list(tab2.schema.names)
637 columns2 = self.butler.get(
638 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
639 )
640 # We check the set because pandas reorders the columns.
641 self.assertEqual(set(columns), set(columns2))
643 # Override the component using a dataset type.
644 columnsType = self.datasetType.makeComponentDatasetType("columns").overrideStorageClass(
645 "ArrowColumnList"
646 )
647 self.assertEqual(columns2, self.butler.get(columnsType))
649 # Check getting a component while overriding the storage class via
650 # the dataset type. This overrides the parent storage class and then
651 # selects the component.
652 columnsType = self.datasetType.overrideStorageClass("ArrowAstropy").makeComponentDatasetType(
653 "columns"
654 )
655 self.assertEqual(columns2, self.butler.get(columnsType))
657 # Check reading the schema.
658 schema = tab2.schema
659 schema2 = self.butler.get(
660 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema"
661 )
663 # These will not have the same metadata, nor will the string column
664 # information be maintained.
665 self.assertEqual(len(schema.names), len(schema2.names))
666 for name in schema.names:
667 if schema.field(name).type not in (pa.string(), pa.binary()):
668 self.assertEqual(schema.field(name).type, schema2.field(name).type)
670 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
671 def testWriteMultiIndexDataFrameReadAsArrowTable(self):
672 df1 = _makeMultiIndexDataFrame()
674 self.butler.put(df1, self.datasetType, dataId={})
676 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
678 tab2_df = arrow_to_pandas(tab2)
679 self.assertTrue(df1.equals(tab2_df))
681 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
682 def testWriteSingleIndexDataFrameReadAsNumpyTable(self):
683 df1, allColumns = _makeSingleIndexDataFrame()
685 self.butler.put(df1, self.datasetType, dataId={})
687 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
689 tab2_df = pd.DataFrame.from_records(tab2, index=["index"])
690 self.assertTrue(df1.equals(tab2_df))
692 # Check reading the columns.
693 columns = list(tab2.dtype.names)
694 columns2 = self.butler.get(
695 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
696 )
697 # We check the set because pandas reorders the columns.
698 self.assertEqual(set(columns2), set(columns))
700 # Check reading the schema.
701 schema = ArrowNumpySchema(tab2.dtype)
702 schema2 = self.butler.get(
703 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema"
704 )
706 # The string types will be objectified by pandas, and the order
707 # will be changed because of pandas indexing.
708 self.assertEqual(len(schema.schema.names), len(schema2.schema.names))
709 for name in schema.schema.names:
710 self.assertIn(name, schema2.schema.names)
711 # It is not possible to properly track string columns via
712 # the schema consistently.
713 if schema.schema[name].type == np.dtype("O") or schema2.schema[name].type == np.dtype("O"):
714 continue
715 else:
716 self.assertEqual(schema2.schema[name].type, schema.schema[name].type)
718 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
719 def testWriteMultiIndexDataFrameReadAsNumpyTable(self):
720 df1 = _makeMultiIndexDataFrame()
722 self.butler.put(df1, self.datasetType, dataId={})
724 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
726 # This is an odd duck, it doesn't really round-trip.
727 # This test simply checks that it's readable, but definitely not
728 # recommended.
730 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.")
731 def testWriteSingleIndexDataFrameReadAsNumpyDict(self):
732 df1, allColumns = _makeSingleIndexDataFrame()
734 self.butler.put(df1, self.datasetType, dataId={})
736 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
738 tab2_df = pd.DataFrame.from_records(tab2, index=["index"])
739 # The column order is not maintained.
740 self.assertEqual(set(df1.columns), set(tab2_df.columns))
741 for col in df1.columns:
742 self.assertTrue(np.all(df1[col].values == tab2_df[col].values))
744 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.")
745 def testWriteMultiIndexDataFrameReadAsNumpyDict(self):
746 df1 = _makeMultiIndexDataFrame()
748 self.butler.put(df1, self.datasetType, dataId={})
750 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
752 # This is an odd duck, it doesn't really round-trip.
753 # This test simply checks that it's readable, but definitely not
754 # recommended.
756 def testBadDataFrameColumnParquet(self):
757 df1, allColumns = _makeSingleIndexDataFrame()
759 # Make a column with mixed type.
760 bad_col1 = [0.0] * len(df1)
761 bad_col1[1] = 0.0 * units.nJy
762 bad_df = df1.copy()
763 bad_df["bad_col1"] = bad_col1
765 # At the moment we cannot check that the correct note is added
766 # to the exception, but that will be possible in the future.
767 with self.assertRaises(RuntimeError):
768 self.butler.put(bad_df, self.datasetType, dataId={})
770 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
771 def testWriteReadAstropyTableLossless(self):
772 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
774 put_ref = self.butler.put(tab1, self.datasetType, dataId={})
776 tab2 = self.butler.get(
777 self.datasetType,
778 dataId={},
779 storageClass="ArrowAstropy",
780 parameters={"strip_astropy_meta_yaml": False},
781 )
783 # Check that minimal provenance was written by default.
784 expected = {
785 "LSST.BUTLER.ID": str(put_ref.id),
786 "LSST.BUTLER.RUN": "test_run",
787 "LSST.BUTLER.DATASETTYPE": "data",
788 "LSST.BUTLER.N_INPUTS": 0,
789 }
791 self.assertEqual(tab2.meta, expected)
793 _checkAstropyTableEquality(tab1, tab2)
795 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
796 def testWriteReadAstropyTableProvenance(self):
797 tab1 = _makeSimpleAstropyTable()
799 # Create a ref for provenance.
800 astropy_type = DatasetType(
801 "astropy_parquet",
802 dimensions=(),
803 storageClass="ArrowAstropy",
804 universe=self.butler.dimensions,
805 )
806 self.butler.registry.registerDatasetType(astropy_type)
807 input_ref = DatasetRef(astropy_type, {}, run="other_run")
808 quantum_id = uuid.uuid4()
809 provenance = DatasetProvenance(quantum_id=quantum_id)
810 provenance.add_input(input_ref)
812 put_ref = self.butler.put(tab1, self.datasetType, dataId={}, provenance=provenance)
814 tab2 = self.butler.get(
815 self.datasetType,
816 dataId={},
817 storageClass="ArrowAstropy",
818 parameters={"strip_astropy_meta_yaml": False},
819 )
821 expected = {
822 "LSST.BUTLER.ID": str(put_ref.id),
823 "LSST.BUTLER.RUN": "test_run",
824 "LSST.BUTLER.DATASETTYPE": "data",
825 "LSST.BUTLER.QUANTUM": str(quantum_id),
826 "LSST.BUTLER.N_INPUTS": 1,
827 "LSST.BUTLER.INPUT.0.ID": str(input_ref.id),
828 "LSST.BUTLER.INPUT.0.RUN": "other_run",
829 "LSST.BUTLER.INPUT.0.DATASETTYPE": "astropy_parquet",
830 }
831 self.assertEqual(tab2.meta, expected)
833 # Put the dataset again, with different provenance and ensure
834 # that the previous provenance was stripped.
835 self.butler.collections.register("new_run")
836 put_ref3 = self.butler.put(tab2, self.datasetType, dataId={}, run="new_run")
838 # tab2 will have been updated in place.
839 expected = {
840 "LSST.BUTLER.ID": str(put_ref3.id),
841 "LSST.BUTLER.RUN": "new_run",
842 "LSST.BUTLER.DATASETTYPE": "data",
843 "LSST.BUTLER.N_INPUTS": 0,
844 }
845 self.assertEqual(tab2.meta, expected)
846 null_prov, prov_ref = DatasetProvenance.from_flat_dict(tab2.meta, self.butler)
847 self.assertEqual(prov_ref, put_ref3)
848 self.assertEqual(null_prov, DatasetProvenance())
850 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
851 def testWriteReadNumpyTableLossless(self):
852 tab1 = _makeSimpleNumpyTable(include_multidim=True)
854 self.butler.put(tab1, self.datasetType, dataId={})
856 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
858 _checkNumpyTableEquality(tab1, tab2)
860 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
861 def testMaskedNumpy(self):
862 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=True)
863 tab1_np = arrow_to_numpy(tab1)
864 self.assertIsInstance(tab1_np, np.ma.MaskedArray)
865 # Stats on a masked column should ignore the nan in row 1.
866 col = tab1_np["m_f8"]
867 self.assertEqual(np.mean(col), 2.25, f"Column: {col}")
869 # Now without a mask.
870 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=False)
871 tab1_np = arrow_to_numpy(tab1)
872 self.assertNotIsInstance(tab1_np, np.ma.MaskedArray)
874 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
875 def testWriteReadArrowTableLossless(self):
876 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=True)
878 self.butler.put(tab1, self.datasetType, dataId={})
880 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
882 self.assertEqual(tab1.schema, tab2.schema)
883 tab1_np = arrow_to_numpy(tab1)
884 tab2_np = arrow_to_numpy(tab2)
885 for col in tab1.column_names:
886 np.testing.assert_array_equal(tab2_np[col], tab1_np[col])
888 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.")
889 def testWriteReadNumpyDictLossless(self):
890 tab1 = _makeSimpleNumpyTable(include_multidim=True)
891 dict1 = _numpy_to_numpy_dict(tab1)
893 self.butler.put(tab1, self.datasetType, dataId={})
895 dict2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
897 _checkNumpyDictEquality(dict1, dict2)
900@unittest.skipUnless(pd is not None, "Cannot test InMemoryDatastore with DataFrames without pandas.")
901class InMemoryDataFrameDelegateTestCase(ParquetFormatterDataFrameTestCase):
902 """Tests for InMemoryDatastore, using ArrowTableDelegate with Dataframe."""
904 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
906 def testBadDataFrameColumnParquet(self):
907 # This test does not raise for an in-memory datastore.
908 pass
910 def testWriteMultiIndexDataFrameReadAsAstropyTable(self):
911 df1 = _makeMultiIndexDataFrame()
913 self.butler.put(df1, self.datasetType, dataId={})
915 with self.assertRaises(ValueError):
916 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
918 def testLegacyDataFrame(self):
919 # This test does not work with an inMemoryDatastore.
920 pass
922 def testBadInput(self):
923 df1, _ = _makeSingleIndexDataFrame()
924 delegate = ArrowTableDelegate("DataFrame")
926 with self.assertRaises(ValueError):
927 delegate.handleParameters(inMemoryDataset="not_a_dataframe")
929 with self.assertRaises(AttributeError):
930 delegate.getComponent(composite=df1, componentName="nothing")
932 def testStorageClass(self):
933 df1, allColumns = _makeSingleIndexDataFrame()
935 factory = StorageClassFactory()
936 factory.addFromConfig(StorageClassConfig())
938 storageClass = factory.findStorageClass(type(df1), compare_types=False)
939 # Force the name lookup to do name matching.
940 storageClass._pytype = None
941 self.assertEqual(storageClass.name, "DataFrame")
943 storageClass = factory.findStorageClass(type(df1), compare_types=True)
944 # Force the name lookup to do name matching.
945 storageClass._pytype = None
946 self.assertEqual(storageClass.name, "DataFrame")
949@unittest.skipUnless(atable is not None, "Cannot test ParquetFormatterArrowAstropy without astropy.")
950@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowAstropy without pyarrow.")
951class ParquetFormatterArrowAstropyTestCase(unittest.TestCase):
952 """Tests for ParquetFormatter, ArrowAstropy, using local file datastore."""
954 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
956 def setUp(self):
957 """Create a new butler root for each test."""
958 self.root = makeTestTempDir(TESTDIR)
959 config = Config(self.configFile)
960 self.run = "test_run"
961 self.butler = Butler.from_config(
962 Butler.makeRepo(self.root, config=config), writeable=True, run=self.run
963 )
964 self.enterContext(self.butler)
965 # No dimensions in dataset type so we don't have to worry about
966 # inserting dimension data or defining data IDs.
967 self.datasetType = DatasetType(
968 "data", dimensions=(), storageClass="ArrowAstropy", universe=self.butler.dimensions
969 )
970 self.butler.registry.registerDatasetType(self.datasetType)
972 def tearDown(self):
973 removeTestTempDir(self.root)
975 def testAstropyTable(self):
976 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
978 self.butler.put(tab1, self.datasetType, dataId={})
979 # Read the whole Table.
980 tab2 = self.butler.get(self.datasetType, dataId={})
981 _checkAstropyTableEquality(tab1, tab2)
982 # Read the columns.
983 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
984 self.assertEqual(len(columns2), len(tab1.dtype.names))
985 for i, name in enumerate(tab1.dtype.names):
986 self.assertEqual(columns2[i], name)
987 # Read the rowcount.
988 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
989 self.assertEqual(rowcount, len(tab1))
990 # Read the schema.
991 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
992 self.assertEqual(schema, ArrowAstropySchema(tab1))
993 # Read just some columns a few different ways.
994 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
995 _checkAstropyTableEquality(tab1[("a", "c")], tab3)
996 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
997 _checkAstropyTableEquality(tab1[("a",)], tab4)
998 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
999 _checkAstropyTableEquality(tab1[("index", "a")], tab5)
1000 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
1001 _checkAstropyTableEquality(tab1[("ddd",)], tab6)
1002 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
1003 _checkAstropyTableEquality(tab1[("a",)], tab7)
1004 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??"]})
1005 _checkAstropyTableEquality(tab1[("ddd", "dtn", "dtu")], tab8)
1006 tab9 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]})
1007 _checkAstropyTableEquality(tab1[("ddd", "dtn", "dtu", "a")], tab9)
1008 # Passing an unrecognized column should be a ValueError.
1009 with self.assertRaises(ValueError):
1010 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
1012 def testAstropyTableBigEndian(self):
1013 tab1 = _makeSimpleAstropyTable(include_bigendian=True)
1015 self.butler.put(tab1, self.datasetType, dataId={})
1016 # Read the whole Table.
1017 tab2 = self.butler.get(self.datasetType, dataId={})
1018 _checkAstropyTableEquality(tab1, tab2, has_bigendian=True)
1020 def testAstropyTableWithMetadata(self):
1021 tab1 = _makeSimpleAstropyTable(include_multidim=True)
1023 meta = {
1024 "meta_a": 5,
1025 "meta_b": 10.0,
1026 "meta_c": [1, 2, 3],
1027 "meta_d": True,
1028 "meta_e": "string",
1029 }
1031 tab1.meta.update(meta)
1033 self.butler.put(tab1, self.datasetType, dataId={})
1034 # Read the whole Table.
1035 tab2 = self.butler.get(self.datasetType, dataId={}, parameters={"strip_astropy_meta_yaml": False})
1036 # This will check that the metadata is equivalent as well.
1037 _checkAstropyTableEquality(tab1, tab2)
1039 def testArrowAstropySchema(self):
1040 tab1 = _makeSimpleAstropyTable()
1041 tab1_arrow = astropy_to_arrow(tab1)
1042 schema = ArrowAstropySchema.from_arrow(tab1_arrow.schema)
1044 self.assertIsInstance(schema.schema, atable.Table)
1045 self.assertEqual(repr(schema), repr(schema._schema))
1046 self.assertNotEqual(schema, "not_a_schema")
1047 self.assertEqual(schema, schema)
1049 # Test various inequalities
1050 tab2 = tab1.copy()
1051 tab2.rename_column("index", "index2")
1052 schema2 = ArrowAstropySchema(tab2)
1053 self.assertNotEqual(schema2, schema)
1055 tab2 = tab1.copy()
1056 tab2["index"].unit = units.micron
1057 schema2 = ArrowAstropySchema(tab2)
1058 self.assertNotEqual(schema2, schema)
1060 tab2 = tab1.copy()
1061 tab2["index"].description = "Index column"
1062 schema2 = ArrowAstropySchema(tab2)
1063 self.assertNotEqual(schema2, schema)
1065 tab2 = tab1.copy()
1066 tab2["index"].format = "%05d"
1067 schema2 = ArrowAstropySchema(tab2)
1068 self.assertNotEqual(schema2, schema)
1070 def testAstropyParquet(self):
1071 tab1 = _makeSimpleAstropyTable()
1073 # Remove datetime column which doesn't work with astropy currently.
1074 del tab1["dtn"]
1075 del tab1["dtu"]
1077 fname = os.path.join(self.root, "test_astropy.parq")
1078 tab1.write(fname)
1080 astropy_type = DatasetType(
1081 "astropy_parquet",
1082 dimensions=(),
1083 storageClass="ArrowAstropy",
1084 universe=self.butler.dimensions,
1085 )
1086 self.butler.registry.registerDatasetType(astropy_type)
1088 data_id = {}
1089 ref = DatasetRef(astropy_type, data_id, run=self.run)
1090 dataset = FileDataset(path=fname, refs=[ref], formatter=ParquetFormatter)
1092 self.butler.ingest(dataset, transfer="copy")
1094 self.butler.put(tab1, self.datasetType, dataId={})
1096 tab2a = self.butler.get(self.datasetType, dataId={})
1097 tab2b = self.butler.get("astropy_parquet", dataId={})
1098 _checkAstropyTableEquality(tab2a, tab2b)
1100 columns2a = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1101 columns2b = self.butler.get("astropy_parquet.columns", dataId={})
1102 self.assertEqual(len(columns2b), len(columns2a))
1103 for i, name in enumerate(columns2a):
1104 self.assertEqual(columns2b[i], name)
1106 rowcount2a = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
1107 rowcount2b = self.butler.get("astropy_parquet.rowcount", dataId={})
1108 self.assertEqual(rowcount2a, rowcount2b)
1110 schema2a = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
1111 schema2b = self.butler.get("astropy_parquet.schema", dataId={})
1112 self.assertEqual(schema2a, schema2b)
1114 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
1115 def testWriteAstropyReadAsArrowTable(self):
1116 # This astropy <-> arrow works fine with masked columns.
1117 tab1 = _makeSimpleAstropyTable(include_masked=True)
1119 self.butler.put(tab1, self.datasetType, dataId={})
1121 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
1123 tab2_astropy = arrow_to_astropy(tab2)
1124 _checkAstropyTableEquality(tab1, tab2_astropy)
1126 # Check reading the columns.
1127 columns = tab2.schema.names
1128 columns2 = self.butler.get(
1129 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1130 )
1131 self.assertEqual(columns2, columns)
1133 # Check reading the schema.
1134 schema = tab2.schema
1135 schema2 = self.butler.get(
1136 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema"
1137 )
1139 self.assertEqual(schema, schema2)
1141 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1142 def testWriteAstropyReadAsDataFrame(self):
1143 tab1 = _makeSimpleAstropyTable()
1145 self.butler.put(tab1, self.datasetType, dataId={})
1147 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1149 # This is tricky because it loses the units and gains a bonus pandas
1150 # _index_ column, so we just test the dataframe form.
1152 tab1_df = tab1.to_pandas()
1153 self.assertTrue(tab1_df.equals(tab2))
1155 # Check reading the columns.
1156 columns = tab2.columns
1157 columns2 = self.butler.get(
1158 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1159 )
1160 self.assertTrue(columns.equals(columns2))
1162 # Check reading the schema.
1163 schema = DataFrameSchema(tab2)
1164 schema2 = self.butler.get(
1165 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1166 )
1168 self.assertEqual(schema2, schema)
1170 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1171 def testWriteAstropyWithMaskedColsReadAsDataFrame(self):
1172 # We need to special-case the write-as-astropy read-as-pandas code
1173 # with masks because pandas has multiple ways to use masked columns.
1174 # (When writing an astropy table with masked columns we get an object
1175 # column back, but each unmasked element has the correct type.)
1176 tab1 = _makeSimpleAstropyTable(include_masked=True)
1178 self.butler.put(tab1, self.datasetType, dataId={})
1180 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1182 tab1_df = astropy_to_pandas(tab1)
1184 self.assertTrue(tab1_df.columns.equals(tab2.columns))
1185 for name in tab2.columns:
1186 col1 = tab1_df[name]
1187 col2 = tab2[name]
1189 if col1.hasnans:
1190 notNull = col1.notnull()
1191 self.assertTrue(notNull.equals(col2.notnull()))
1192 # Need to check value-by-value because column may
1193 # be made of objects, depending on what pandas decides.
1194 for index in notNull.values.nonzero()[0]:
1195 self.assertEqual(col1[index], col2[index])
1196 else:
1197 self.assertTrue(col1.equals(col2))
1199 @unittest.skipUnless(pd is not None, "Cannot test writing as a dataframe without pandas.")
1200 def testWriteSingleIndexDataFrameWithMaskedColsReadAsAstropyTable(self):
1201 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True)
1203 self.butler.put(df1, self.datasetType, dataId={})
1205 tab2 = self.butler.get(self.datasetType, dataId={})
1207 df1_tab = pandas_to_astropy(df1)
1209 _checkAstropyTableEquality(df1_tab, tab2)
1211 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1212 def testWriteAstropyReadAsNumpyTable(self):
1213 tab1 = _makeSimpleAstropyTable()
1214 self.butler.put(tab1, self.datasetType, dataId={})
1216 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
1218 # This is tricky because it loses the units.
1219 tab2_astropy = atable.Table(tab2)
1221 _checkAstropyTableEquality(tab1, tab2_astropy, skip_units=True)
1223 # Check reading the columns.
1224 columns = list(tab2.dtype.names)
1225 columns2 = self.butler.get(
1226 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1227 )
1228 self.assertEqual(columns2, columns)
1230 # Check reading the schema.
1231 schema = ArrowNumpySchema(tab2.dtype)
1232 schema2 = self.butler.get(
1233 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema"
1234 )
1236 self.assertEqual(schema2, schema)
1238 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1239 def testWriteAstropyReadAsNumpyDict(self):
1240 tab1 = _makeSimpleAstropyTable()
1241 self.butler.put(tab1, self.datasetType, dataId={})
1243 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
1245 # This is tricky because it loses the units.
1246 tab2_astropy = atable.Table(tab2)
1248 _checkAstropyTableEquality(tab1, tab2_astropy, skip_units=True)
1250 def testBadAstropyColumnParquet(self):
1251 tab1 = _makeSimpleAstropyTable()
1253 # Make a column with mixed type.
1254 bad_col1 = [0.0] * len(tab1)
1255 bad_col1[1] = 0.0 * units.nJy
1256 bad_tab = tab1.copy()
1257 bad_tab["bad_col1"] = bad_col1
1259 # At the moment we cannot check that the correct note is added
1260 # to the exception, but that will be possible in the future.
1261 with self.assertRaises(RuntimeError):
1262 self.butler.put(bad_tab, self.datasetType, dataId={})
1264 # Make a column with ragged size.
1265 bad_col2 = [[0]] * len(tab1)
1266 bad_col2[1] = [0, 0]
1267 bad_tab = tab1.copy()
1268 bad_tab["bad_col2"] = bad_col2
1270 with self.assertRaises(RuntimeError):
1271 self.butler.put(bad_tab, self.datasetType, dataId={})
1273 @unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.")
1274 def testWriteAstropyTableWithPandasIndexHint(self, testStrip=True):
1275 tab1 = _makeSimpleAstropyTable()
1277 add_pandas_index_to_astropy(tab1, "index")
1279 self.butler.put(tab1, self.datasetType, dataId={})
1281 # Read in as an astropy table and ensure index hint is still there.
1282 tab2 = self.butler.get(self.datasetType, dataId={})
1284 self.assertIn(ASTROPY_PANDAS_INDEX_KEY, tab2.meta)
1285 self.assertEqual(tab2.meta[ASTROPY_PANDAS_INDEX_KEY], "index")
1287 # Read as a dataframe and ensure index is set.
1288 df3 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1290 self.assertEqual(df3.index.name, "index")
1292 # Read as a dataframe without naming the index column.
1293 with self.assertLogs(level="WARNING") as cm:
1294 _ = self.butler.get(
1295 self.datasetType,
1296 dataId={},
1297 storageClass="DataFrame",
1298 parameters={"columns": ["a", "b"]},
1299 )
1300 self.assertIn("Index column ``index``", cm.output[0])
1302 if testStrip:
1303 # Read as an astropy table without naming the index column.
1304 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "b"]})
1306 self.assertNotIn(ASTROPY_PANDAS_INDEX_KEY, tab5.meta)
1308 with self.assertRaises(ValueError):
1309 add_pandas_index_to_astropy(tab1, "not_a_column")
1312@unittest.skipUnless(atable is not None, "Cannot test InMemoryDatastore with AstropyTable without astropy.")
1313class InMemoryArrowAstropyDelegateTestCase(ParquetFormatterArrowAstropyTestCase):
1314 """Tests for InMemoryDatastore, using ArrowTableDelegate with
1315 AstropyTable.
1316 """
1318 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
1320 def testAstropyParquet(self):
1321 # This test does not work with an inMemoryDatastore.
1322 pass
1324 def testBadAstropyColumnParquet(self):
1325 # This test does not raise for an in-memory datastore.
1326 pass
1328 def testBadInput(self):
1329 tab1 = _makeSimpleAstropyTable()
1330 delegate = ArrowTableDelegate("ArrowAstropy")
1332 with self.assertRaises(ValueError):
1333 delegate.handleParameters(inMemoryDataset="not_an_astropy_table")
1335 with self.assertRaises(NotImplementedError):
1336 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]})
1338 with self.assertRaises(AttributeError):
1339 delegate.getComponent(composite=tab1, componentName="nothing")
1341 @unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.")
1342 def testWriteAstropyTableWithPandasIndexHint(self):
1343 super().testWriteAstropyTableWithPandasIndexHint(testStrip=False)
1346@unittest.skipUnless(np is not None, "Cannot test ParquetFormatterArrowNumpy without numpy.")
1347@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowNumpy without pyarrow.")
1348class ParquetFormatterArrowNumpyTestCase(unittest.TestCase):
1349 """Tests for ParquetFormatter, ArrowNumpy, using local file datastore."""
1351 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
1353 def setUp(self):
1354 """Create a new butler root for each test."""
1355 self.root = makeTestTempDir(TESTDIR)
1356 config = Config(self.configFile)
1357 self.butler = Butler.from_config(
1358 Butler.makeRepo(self.root, config=config), writeable=True, run="test_run"
1359 )
1360 self.enterContext(self.butler)
1361 # No dimensions in dataset type so we don't have to worry about
1362 # inserting dimension data or defining data IDs.
1363 self.datasetType = DatasetType(
1364 "data", dimensions=(), storageClass="ArrowNumpy", universe=self.butler.dimensions
1365 )
1366 self.butler.registry.registerDatasetType(self.datasetType)
1368 def tearDown(self):
1369 removeTestTempDir(self.root)
1371 def testNumpyTable(self):
1372 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1374 self.butler.put(tab1, self.datasetType, dataId={})
1375 # Read the whole Table.
1376 tab2 = self.butler.get(self.datasetType, dataId={})
1377 _checkNumpyTableEquality(tab1, tab2)
1378 # Read the columns.
1379 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1380 self.assertEqual(len(columns2), len(tab1.dtype.names))
1381 for i, name in enumerate(tab1.dtype.names):
1382 self.assertEqual(columns2[i], name)
1383 # Read the rowcount.
1384 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
1385 self.assertEqual(rowcount, len(tab1))
1386 # Read the schema.
1387 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
1388 self.assertEqual(schema, ArrowNumpySchema(tab1.dtype))
1389 # Read just some columns a few different ways.
1390 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
1391 _checkNumpyTableEquality(tab1[["a", "c"]], tab3)
1392 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
1393 _checkNumpyTableEquality(
1394 tab1[
1395 [
1396 "a",
1397 ]
1398 ],
1399 tab4,
1400 )
1401 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
1402 _checkNumpyTableEquality(tab1[["index", "a"]], tab5)
1403 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
1404 _checkNumpyTableEquality(
1405 tab1[
1406 [
1407 "ddd",
1408 ]
1409 ],
1410 tab6,
1411 )
1412 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
1413 _checkNumpyTableEquality(
1414 tab1[
1415 [
1416 "a",
1417 ]
1418 ],
1419 tab7,
1420 )
1421 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]})
1422 _checkNumpyTableEquality(
1423 tab1[
1424 [
1425 "ddd",
1426 "dtn",
1427 "dtu",
1428 "a",
1429 ]
1430 ],
1431 tab8,
1432 )
1433 # Passing an unrecognized column should be a ValueError.
1434 with self.assertRaises(ValueError):
1435 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
1437 def testNumpyTableBigEndian(self):
1438 tab1 = _makeSimpleNumpyTable(include_bigendian=True)
1440 self.butler.put(tab1, self.datasetType, dataId={})
1441 # Read the whole Table.
1442 tab2 = self.butler.get(self.datasetType, dataId={})
1443 _checkNumpyTableEquality(tab1, tab2, has_bigendian=True)
1445 def testArrowNumpySchema(self):
1446 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1447 tab1_arrow = numpy_to_arrow(tab1)
1448 schema = ArrowNumpySchema.from_arrow(tab1_arrow.schema)
1450 self.assertIsInstance(schema.schema, np.dtype)
1451 self.assertEqual(repr(schema), repr(schema._dtype))
1452 self.assertNotEqual(schema, "not_a_schema")
1453 self.assertEqual(schema, schema)
1455 # Test inequality
1456 tab2 = tab1.copy()
1457 names = list(tab2.dtype.names)
1458 names[0] = "index2"
1459 tab2.dtype.names = names
1460 schema2 = ArrowNumpySchema(tab2.dtype)
1461 self.assertNotEqual(schema2, schema)
1463 @unittest.skipUnless(pa is not None, "Cannot test arrow conversions without pyarrow.")
1464 def testNumpyDictConversions(self):
1465 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1467 # Verify that everything round-trips, including the schema.
1468 tab1_arrow = numpy_to_arrow(tab1)
1469 tab1_dict = arrow_to_numpy_dict(tab1_arrow)
1470 tab1_dict_arrow = numpy_dict_to_arrow(tab1_dict)
1472 self.assertEqual(tab1_arrow.schema, tab1_dict_arrow.schema)
1473 self.assertEqual(tab1_arrow, tab1_dict_arrow)
1475 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
1476 def testWriteNumpyTableReadAsArrowTable(self):
1477 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1479 self.butler.put(tab1, self.datasetType, dataId={})
1481 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
1483 tab2_numpy = arrow_to_numpy(tab2)
1485 _checkNumpyTableEquality(tab1, tab2_numpy)
1487 # Check reading the columns.
1488 columns = tab2.schema.names
1489 columns2 = self.butler.get(
1490 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1491 )
1492 self.assertEqual(columns2, columns)
1494 # Check reading the schema.
1495 schema = tab2.schema
1496 schema2 = self.butler.get(
1497 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema"
1498 )
1499 self.assertEqual(schema2, schema)
1501 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1502 def testWriteNumpyTableReadAsDataFrame(self):
1503 tab1 = _makeSimpleNumpyTable()
1505 self.butler.put(tab1, self.datasetType, dataId={})
1507 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1509 # Converting this back to numpy gets confused with the index column
1510 # and changes the datatype of the string column.
1512 tab1_df = pd.DataFrame(tab1)
1514 self.assertTrue(tab1_df.equals(tab2))
1516 # Check reading the columns.
1517 columns = tab2.columns
1518 columns2 = self.butler.get(
1519 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1520 )
1521 self.assertTrue(columns.equals(columns2))
1523 # Check reading the schema.
1524 schema = DataFrameSchema(tab2)
1525 schema2 = self.butler.get(
1526 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1527 )
1529 self.assertEqual(schema2, schema)
1531 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1532 def testWriteNumpyTableReadAsAstropyTable(self):
1533 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1535 self.butler.put(tab1, self.datasetType, dataId={})
1537 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1538 tab2_numpy = tab2.as_array()
1540 _checkNumpyTableEquality(tab1, tab2_numpy)
1542 # Check reading the columns.
1543 columns = list(tab2.columns.keys())
1544 columns2 = self.butler.get(
1545 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1546 )
1547 self.assertEqual(columns2, columns)
1549 # Check reading the schema.
1550 schema = ArrowAstropySchema(tab2)
1551 schema2 = self.butler.get(
1552 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema"
1553 )
1555 self.assertEqual(schema2, schema)
1557 def testWriteNumpyTableReadAsNumpyDict(self):
1558 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1560 self.butler.put(tab1, self.datasetType, dataId={})
1562 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
1563 tab2_numpy = _numpy_dict_to_numpy(tab2)
1565 _checkNumpyTableEquality(tab1, tab2_numpy)
1567 def testBadNumpyColumnParquet(self):
1568 tab1 = _makeSimpleAstropyTable()
1570 # Make a column with mixed type.
1571 bad_col1 = [0.0] * len(tab1)
1572 bad_col1[1] = 0.0 * units.nJy
1573 bad_tab = tab1.copy()
1574 bad_tab["bad_col1"] = bad_col1
1576 bad_tab_np = bad_tab.as_array()
1578 # At the moment we cannot check that the correct note is added
1579 # to the exception, but that will be possible in the future.
1580 with self.assertRaises(RuntimeError):
1581 self.butler.put(bad_tab_np, self.datasetType, dataId={})
1583 # Make a column with ragged size.
1584 bad_col2 = [[0]] * len(tab1)
1585 bad_col2[1] = [0, 0]
1586 bad_tab = tab1.copy()
1587 bad_tab["bad_col2"] = bad_col2
1589 bad_tab_np = bad_tab.as_array()
1591 with self.assertRaises(RuntimeError):
1592 self.butler.put(bad_tab_np, self.datasetType, dataId={})
1594 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1595 def testWriteReadAstropyTableLossless(self):
1596 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
1598 self.butler.put(tab1, self.datasetType, dataId={})
1600 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1602 _checkAstropyTableEquality(tab1, tab2)
1605@unittest.skipUnless(np is not None, "Cannot test ImMemoryDatastore with Numpy table without numpy.")
1606class InMemoryArrowNumpyDelegateTestCase(ParquetFormatterArrowNumpyTestCase):
1607 """Tests for InMemoryDatastore, using ArrowTableDelegate with
1608 Numpy table.
1609 """
1611 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
1613 def testBadNumpyColumnParquet(self):
1614 # This test does not raise for an in-memory datastore.
1615 pass
1617 def testBadInput(self):
1618 tab1 = _makeSimpleNumpyTable()
1619 delegate = ArrowTableDelegate("ArrowNumpy")
1621 with self.assertRaises(ValueError):
1622 delegate.handleParameters(inMemoryDataset="not_a_numpy_table")
1624 with self.assertRaises(NotImplementedError):
1625 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]})
1627 with self.assertRaises(AttributeError):
1628 delegate.getComponent(composite=tab1, componentName="nothing")
1630 def testStorageClass(self):
1631 tab1 = _makeSimpleNumpyTable()
1633 factory = StorageClassFactory()
1634 factory.addFromConfig(StorageClassConfig())
1636 storageClass = factory.findStorageClass(type(tab1), compare_types=False)
1637 # Force the name lookup to do name matching.
1638 storageClass._pytype = None
1639 self.assertEqual(storageClass.name, "ArrowNumpy")
1641 storageClass = factory.findStorageClass(type(tab1), compare_types=True)
1642 # Force the name lookup to do name matching.
1643 storageClass._pytype = None
1644 self.assertEqual(storageClass.name, "ArrowNumpy")
1647@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowTable without pyarrow.")
1648class ParquetFormatterArrowTableTestCase(unittest.TestCase):
1649 """Tests for ParquetFormatter, ArrowTable, using local file datastore."""
1651 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
1653 def setUp(self):
1654 """Create a new butler root for each test."""
1655 self.root = makeTestTempDir(TESTDIR)
1656 config = Config(self.configFile)
1657 self.butler = Butler.from_config(
1658 Butler.makeRepo(self.root, config=config), writeable=True, run="test_run"
1659 )
1660 self.enterContext(self.butler)
1661 # No dimensions in dataset type so we don't have to worry about
1662 # inserting dimension data or defining data IDs.
1663 self.datasetType = DatasetType(
1664 "data", dimensions=(), storageClass="ArrowTable", universe=self.butler.dimensions
1665 )
1666 self.butler.registry.registerDatasetType(self.datasetType)
1668 def tearDown(self):
1669 removeTestTempDir(self.root)
1671 def testArrowTable(self):
1672 tab1 = _makeSimpleArrowTable(include_multidim=True, include_masked=True)
1674 self.butler.put(tab1, self.datasetType, dataId={})
1675 # Read the whole Table.
1676 tab2 = self.butler.get(self.datasetType, dataId={})
1677 # We convert to use the numpy testing framework to handle nan
1678 # comparisons.
1679 self.assertEqual(tab1.schema, tab2.schema)
1680 tab1_np = arrow_to_numpy(tab1)
1681 tab2_np = arrow_to_numpy(tab2)
1682 for col in tab1.column_names:
1683 np.testing.assert_array_equal(tab2_np[col], tab1_np[col])
1684 # Read the columns.
1685 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1686 self.assertEqual(len(columns2), len(tab1.schema.names))
1687 for i, name in enumerate(tab1.schema.names):
1688 self.assertEqual(columns2[i], name)
1689 # Read the rowcount.
1690 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
1691 self.assertEqual(rowcount, len(tab1))
1692 # Read the schema.
1693 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
1694 self.assertEqual(schema, tab1.schema)
1695 # Read just some columns a few different ways.
1696 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
1697 self.assertEqual(tab3, tab1.select(("a", "c")))
1698 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
1699 self.assertEqual(tab4, tab1.select(("a",)))
1700 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
1701 self.assertEqual(tab5, tab1.select(("index", "a")))
1702 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
1703 self.assertEqual(tab6, tab1.select(("ddd",)))
1704 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
1705 self.assertEqual(tab7, tab1.select(("a",)))
1706 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a*", "d??"]})
1707 self.assertEqual(tab8, tab1.select(("a", "ddd", "dtn", "dtu")))
1708 # Passing an unrecognized column should be a ValueError.
1709 with self.assertRaises(ValueError):
1710 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
1712 def testEmptyArrowTable(self):
1713 data = _makeSimpleNumpyTable()
1714 type_list = _numpy_dtype_to_arrow_types(data.dtype)
1716 schema = pa.schema(type_list)
1717 arrays = [[]] * len(schema.names)
1719 tab1 = pa.Table.from_arrays(arrays, schema=schema)
1721 self.butler.put(tab1, self.datasetType, dataId={})
1722 tab2 = self.butler.get(self.datasetType, dataId={})
1723 self.assertEqual(tab2, tab1)
1725 tab1_numpy = arrow_to_numpy(tab1)
1726 self.assertEqual(len(tab1_numpy), 0)
1727 tab1_numpy_arrow = numpy_to_arrow(tab1_numpy)
1728 self.assertEqual(tab1_numpy_arrow, tab1)
1730 tab1_pandas = arrow_to_pandas(tab1)
1731 self.assertEqual(len(tab1_pandas), 0)
1732 tab1_pandas_arrow = pandas_to_arrow(tab1_pandas)
1733 # Unfortunately, string/byte columns get mangled when translated
1734 # through empty pandas dataframes.
1735 self.assertEqual(
1736 tab1_pandas_arrow.select(("index", "a", "b", "c", "ddd")),
1737 tab1.select(("index", "a", "b", "c", "ddd")),
1738 )
1740 tab1_astropy = arrow_to_astropy(tab1)
1741 self.assertEqual(len(tab1_astropy), 0)
1742 tab1_astropy_arrow = astropy_to_arrow(tab1_astropy)
1743 self.assertEqual(tab1_astropy_arrow, tab1)
1745 def testEmptyArrowTableMultidim(self):
1746 data = _makeSimpleNumpyTable(include_multidim=True)
1747 type_list = _numpy_dtype_to_arrow_types(data.dtype)
1749 md = {}
1750 for name in data.dtype.names:
1751 _append_numpy_multidim_metadata(md, name, data.dtype[name])
1753 schema = pa.schema(type_list, metadata=md)
1754 arrays = [[]] * len(schema.names)
1756 tab1 = pa.Table.from_arrays(arrays, schema=schema)
1758 self.butler.put(tab1, self.datasetType, dataId={})
1759 tab2 = self.butler.get(self.datasetType, dataId={})
1760 self.assertEqual(tab2, tab1)
1762 tab1_numpy = arrow_to_numpy(tab1)
1763 self.assertEqual(len(tab1_numpy), 0)
1764 tab1_numpy_arrow = numpy_to_arrow(tab1_numpy)
1765 self.assertEqual(tab1_numpy_arrow, tab1)
1767 tab1_astropy = arrow_to_astropy(tab1)
1768 self.assertEqual(len(tab1_astropy), 0)
1769 tab1_astropy_arrow = astropy_to_arrow(tab1_astropy)
1770 self.assertEqual(tab1_astropy_arrow, tab1)
1772 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1773 def testWriteArrowTableReadAsSingleIndexDataFrame(self):
1774 df1, allColumns = _makeSingleIndexDataFrame()
1776 self.butler.put(df1, self.datasetType, dataId={})
1778 # Read back out as a dataframe.
1779 df2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1780 self.assertTrue(df1.equals(df2))
1782 # Read back out as an arrow table, convert to dataframe.
1783 tab3 = self.butler.get(self.datasetType, dataId={})
1784 df3 = arrow_to_pandas(tab3)
1785 self.assertTrue(df1.equals(df3))
1787 # Check reading the columns.
1788 columns = df2.reset_index().columns
1789 columns2 = self.butler.get(
1790 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1791 )
1792 # We check the set because pandas reorders the columns.
1793 self.assertEqual(set(columns2.to_list()), set(columns.to_list()))
1795 # Check reading the schema.
1796 schema = DataFrameSchema(df1)
1797 schema2 = self.butler.get(
1798 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1799 )
1800 self.assertEqual(schema2, schema)
1802 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
1803 def testWriteArrowTableReadAsMultiIndexDataFrame(self):
1804 df1 = _makeMultiIndexDataFrame()
1806 self.butler.put(df1, self.datasetType, dataId={})
1808 # Read back out as a dataframe.
1809 df2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
1810 self.assertTrue(df1.equals(df2))
1812 # Read back out as an arrow table, convert to dataframe.
1813 atab3 = self.butler.get(self.datasetType, dataId={})
1814 df3 = arrow_to_pandas(atab3)
1815 self.assertTrue(df1.equals(df3))
1817 # Check reading the columns.
1818 columns = df2.columns
1819 columns2 = self.butler.get(
1820 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex"
1821 )
1822 self.assertTrue(columns2.equals(columns))
1824 # Check reading the schema.
1825 schema = DataFrameSchema(df1)
1826 schema2 = self.butler.get(
1827 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema"
1828 )
1829 self.assertEqual(schema2, schema)
1831 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1832 def testWriteArrowTableReadAsAstropyTable(self):
1833 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
1835 self.butler.put(tab1, self.datasetType, dataId={})
1837 # Read back out as an astropy table.
1838 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1839 _checkAstropyTableEquality(tab1, tab2)
1841 # Read back out as an arrow table, convert to astropy table.
1842 atab3 = self.butler.get(self.datasetType, dataId={})
1843 tab3 = arrow_to_astropy(atab3)
1844 _checkAstropyTableEquality(tab1, tab3)
1846 # Check reading the columns.
1847 columns = list(tab2.columns.keys())
1848 columns2 = self.butler.get(
1849 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1850 )
1851 self.assertEqual(columns2, columns)
1853 # Check reading the schema.
1854 schema = ArrowAstropySchema(tab1)
1855 schema2 = self.butler.get(
1856 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema"
1857 )
1858 self.assertEqual(schema2, schema)
1860 # Check the schema conversions and units.
1861 arrow_schema = schema.to_arrow_schema()
1862 for name in arrow_schema.names:
1863 field_metadata = arrow_schema.field(name).metadata
1864 if (
1865 b"description" in field_metadata
1866 and (description := field_metadata[b"description"].decode("UTF-8")) != ""
1867 ):
1868 self.assertEqual(schema2.schema[name].description, description)
1869 else:
1870 self.assertIsNone(schema2.schema[name].description)
1871 if b"unit" in field_metadata and (unit := field_metadata[b"unit"].decode("UTF-8")) != "":
1872 self.assertEqual(schema2.schema[name].unit, units.Unit(unit))
1874 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1875 def testWriteArrowTableReadAsNumpyTable(self):
1876 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1878 self.butler.put(tab1, self.datasetType, dataId={})
1880 # Read back out as a numpy table.
1881 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
1882 _checkNumpyTableEquality(tab1, tab2)
1884 # Read back out as an arrow table, convert to numpy table.
1885 atab3 = self.butler.get(self.datasetType, dataId={})
1886 tab3 = arrow_to_numpy(atab3)
1887 _checkNumpyTableEquality(tab1, tab3)
1889 # Check reading the columns.
1890 columns = list(tab2.dtype.names)
1891 columns2 = self.butler.get(
1892 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList"
1893 )
1894 self.assertEqual(columns2, columns)
1896 # Check reading the schema.
1897 schema = ArrowNumpySchema(tab1.dtype)
1898 schema2 = self.butler.get(
1899 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema"
1900 )
1901 self.assertEqual(schema2, schema)
1903 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.")
1904 def testWriteArrowTableReadAsNumpyDict(self):
1905 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1907 self.butler.put(tab1, self.datasetType, dataId={})
1909 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict")
1910 tab2_numpy = _numpy_dict_to_numpy(tab2)
1911 _checkNumpyTableEquality(tab1, tab2_numpy)
1913 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
1914 def testWriteReadAstropyTableLossless(self):
1915 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
1917 self.butler.put(tab1, self.datasetType, dataId={})
1919 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
1921 _checkAstropyTableEquality(tab1, tab2)
1924@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with ArroWTable without pyarrow.")
1925class InMemoryArrowTableDelegateTestCase(ParquetFormatterArrowTableTestCase):
1926 """Tests for InMemoryDatastore, using ArrowTableDelegate."""
1928 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
1930 def testBadInput(self):
1931 tab1 = _makeSimpleArrowTable()
1932 delegate = ArrowTableDelegate("ArrowTable")
1934 with self.assertRaises(ValueError):
1935 delegate.handleParameters(inMemoryDataset="not_an_arrow_table")
1937 with self.assertRaises(NotImplementedError):
1938 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]})
1940 with self.assertRaises(AttributeError):
1941 delegate.getComponent(composite=tab1, componentName="nothing")
1943 def testStorageClass(self):
1944 tab1 = _makeSimpleArrowTable()
1946 factory = StorageClassFactory()
1947 factory.addFromConfig(StorageClassConfig())
1949 storageClass = factory.findStorageClass(type(tab1), compare_types=False)
1950 # Force the name lookup to do name matching.
1951 storageClass._pytype = None
1952 self.assertEqual(storageClass.name, "ArrowTable")
1954 storageClass = factory.findStorageClass(type(tab1), compare_types=True)
1955 # Force the name lookup to do name matching.
1956 storageClass._pytype = None
1957 self.assertEqual(storageClass.name, "ArrowTable")
1960@unittest.skipUnless(np is not None, "Cannot test ParquetFormatterArrowNumpy without numpy.")
1961@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowNumpy without pyarrow.")
1962class ParquetFormatterArrowNumpyDictTestCase(unittest.TestCase):
1963 """Tests for ParquetFormatter, ArrowNumpyDict, using local file store."""
1965 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
1967 def setUp(self):
1968 """Create a new butler root for each test."""
1969 self.root = makeTestTempDir(TESTDIR)
1970 config = Config(self.configFile)
1971 self.butler = Butler.from_config(
1972 Butler.makeRepo(self.root, config=config), writeable=True, run="test_run"
1973 )
1974 self.enterContext(self.butler)
1975 # No dimensions in dataset type so we don't have to worry about
1976 # inserting dimension data or defining data IDs.
1977 self.datasetType = DatasetType(
1978 "data", dimensions=(), storageClass="ArrowNumpyDict", universe=self.butler.dimensions
1979 )
1980 self.butler.registry.registerDatasetType(self.datasetType)
1982 def tearDown(self):
1983 removeTestTempDir(self.root)
1985 def testNumpyDict(self):
1986 tab1 = _makeSimpleNumpyTable(include_multidim=True)
1987 dict1 = _numpy_to_numpy_dict(tab1)
1989 self.butler.put(dict1, self.datasetType, dataId={})
1990 # Read the whole table.
1991 dict2 = self.butler.get(self.datasetType, dataId={})
1992 _checkNumpyDictEquality(dict1, dict2)
1993 # Read the columns.
1994 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
1995 self.assertEqual(len(columns2), len(dict1.keys()))
1996 for name in dict1:
1997 self.assertIn(name, columns2)
1998 # Read the rowcount.
1999 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
2000 self.assertEqual(rowcount, len(dict1["a"]))
2001 # Read the schema.
2002 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
2003 self.assertEqual(schema, ArrowNumpySchema(tab1.dtype))
2004 # Read just some columns a few different ways.
2005 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
2006 subdict = {key: dict1[key] for key in ["a", "c"]}
2007 _checkNumpyDictEquality(subdict, tab3)
2008 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
2009 subdict = {key: dict1[key] for key in ["a"]}
2010 _checkNumpyDictEquality(subdict, tab4)
2011 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
2012 subdict = {key: dict1[key] for key in ["index", "a"]}
2013 _checkNumpyDictEquality(subdict, tab5)
2014 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
2015 subdict = {key: dict1[key] for key in ["ddd"]}
2016 _checkNumpyDictEquality(subdict, tab6)
2017 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
2018 subdict = {key: dict1[key] for key in ["a"]}
2019 _checkNumpyDictEquality(subdict, tab7)
2020 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]})
2021 subdict = {key: dict1[key] for key in ["ddd", "dtn", "dtu", "a"]}
2022 _checkNumpyDictEquality(subdict, tab8)
2023 # Passing an unrecognized column should be a ValueError.
2024 with self.assertRaises(ValueError):
2025 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
2027 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.")
2028 def testWriteNumpyDictReadAsArrowTable(self):
2029 tab1 = _makeSimpleNumpyTable(include_multidim=True)
2030 dict1 = _numpy_to_numpy_dict(tab1)
2032 self.butler.put(dict1, self.datasetType, dataId={})
2034 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable")
2036 tab2_dict = arrow_to_numpy_dict(tab2)
2038 _checkNumpyDictEquality(dict1, tab2_dict)
2040 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.")
2041 def testWriteNumpyDictReadAsDataFrame(self):
2042 tab1 = _makeSimpleNumpyTable()
2043 dict1 = _numpy_to_numpy_dict(tab1)
2045 self.butler.put(dict1, self.datasetType, dataId={})
2047 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame")
2049 # The order of the dict may get mixed up, so we need to check column
2050 # by column. We also need to do this in dataframe form because pandas
2051 # changes the datatype of the string column.
2052 tab1_df = pd.DataFrame(tab1)
2054 self.assertEqual(set(tab1_df.columns), set(tab2.columns))
2055 for col in tab1_df.columns:
2056 self.assertTrue(np.all(tab1_df[col].values == tab2[col].values))
2058 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
2059 def testWriteNumpyDictReadAsAstropyTable(self):
2060 tab1 = _makeSimpleNumpyTable(include_multidim=True)
2061 dict1 = _numpy_to_numpy_dict(tab1)
2063 self.butler.put(dict1, self.datasetType, dataId={})
2065 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
2066 tab2_dict = _astropy_to_numpy_dict(tab2)
2068 _checkNumpyDictEquality(dict1, tab2_dict)
2070 def testWriteNumpyDictReadAsNumpyTable(self):
2071 tab1 = _makeSimpleNumpyTable(include_multidim=True)
2072 dict1 = _numpy_to_numpy_dict(tab1)
2074 self.butler.put(dict1, self.datasetType, dataId={})
2076 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy")
2077 tab2_dict = _numpy_to_numpy_dict(tab2)
2079 _checkNumpyDictEquality(dict1, tab2_dict)
2081 def testWriteNumpyDictBad(self):
2082 dict1 = {"a": 4, "b": np.ndarray([1])}
2083 with self.assertRaises(RuntimeError):
2084 self.butler.put(dict1, self.datasetType, dataId={})
2086 dict2 = {"a": np.zeros(4), "b": np.zeros(5)}
2087 with self.assertRaises(RuntimeError):
2088 self.butler.put(dict2, self.datasetType, dataId={})
2090 dict3 = {"a": [0] * 5, "b": np.zeros(5)}
2091 with self.assertRaises(RuntimeError):
2092 self.butler.put(dict3, self.datasetType, dataId={})
2094 dict4 = {"a": np.zeros(4), "b": np.zeros(4, dtype="O")}
2095 with self.assertRaises(RuntimeError):
2096 self.butler.put(dict4, self.datasetType, dataId={})
2098 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.")
2099 def testWriteReadAstropyTableLossless(self):
2100 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True)
2102 self.butler.put(tab1, self.datasetType, dataId={})
2104 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy")
2106 _checkAstropyTableEquality(tab1, tab2)
2109@unittest.skipUnless(np is not None, "Cannot test InMemoryDatastore with NumpyDict without numpy.")
2110@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with NumpyDict without pyarrow.")
2111class InMemoryNumpyDictDelegateTestCase(ParquetFormatterArrowNumpyDictTestCase):
2112 """Tests for InMemoryDatastore, using ArrowTableDelegate with
2113 Numpy dict.
2114 """
2116 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
2118 def testWriteNumpyDictBad(self):
2119 # The sub-type checking is not done on in-memory datastore.
2120 pass
2123@unittest.skipUnless(pa is not None, "Cannot test ArrowSchema without pyarrow.")
2124class ParquetFormatterArrowSchemaTestCase(unittest.TestCase):
2125 """Tests for ParquetFormatter, ArrowSchema, using local file datastore."""
2127 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml")
2129 def setUp(self):
2130 """Create a new butler root for each test."""
2131 self.root = makeTestTempDir(TESTDIR)
2132 config = Config(self.configFile)
2133 self.butler = Butler.from_config(
2134 Butler.makeRepo(self.root, config=config), writeable=True, run="test_run"
2135 )
2136 self.enterContext(self.butler)
2137 # No dimensions in dataset type so we don't have to worry about
2138 # inserting dimension data or defining data IDs.
2139 self.datasetType = DatasetType(
2140 "data", dimensions=(), storageClass="ArrowSchema", universe=self.butler.dimensions
2141 )
2142 self.butler.registry.registerDatasetType(self.datasetType)
2144 def tearDown(self):
2145 removeTestTempDir(self.root)
2147 def _makeTestSchema(self):
2148 schema = pa.schema(
2149 [
2150 pa.field(
2151 "int32",
2152 pa.int32(),
2153 nullable=False,
2154 metadata={
2155 "description": "32-bit integer",
2156 "unit": "",
2157 },
2158 ),
2159 pa.field(
2160 "int64",
2161 pa.int64(),
2162 nullable=False,
2163 metadata={
2164 "description": "64-bit integer",
2165 "unit": "",
2166 },
2167 ),
2168 pa.field(
2169 "uint64",
2170 pa.uint64(),
2171 nullable=False,
2172 metadata={
2173 "description": "64-bit unsigned integer",
2174 "unit": "",
2175 },
2176 ),
2177 pa.field(
2178 "float32",
2179 pa.float32(),
2180 nullable=False,
2181 metadata={
2182 "description": "32-bit float",
2183 "unit": "count",
2184 },
2185 ),
2186 pa.field(
2187 "float64",
2188 pa.float64(),
2189 nullable=False,
2190 metadata={
2191 "description": "64-bit float",
2192 "unit": "nJy",
2193 },
2194 ),
2195 pa.field(
2196 "fixed_size_list",
2197 pa.list_(pa.float64(), list_size=10),
2198 nullable=False,
2199 metadata={
2200 "description": "Fixed size list of 64-bit floats.",
2201 "unit": "nJy",
2202 },
2203 ),
2204 pa.field(
2205 "variable_size_list",
2206 pa.list_(pa.float64()),
2207 nullable=False,
2208 metadata={
2209 "description": "Variable size list of 64-bit floats.",
2210 "unit": "nJy",
2211 },
2212 ),
2213 # One of these fields will have no description.
2214 pa.field(
2215 "string",
2216 pa.string(),
2217 nullable=False,
2218 metadata={
2219 "unit": "",
2220 },
2221 ),
2222 # One of these fields will have no metadata.
2223 pa.field(
2224 "binary",
2225 pa.binary(),
2226 nullable=False,
2227 ),
2228 ]
2229 )
2231 return schema
2233 def testArrowSchema(self):
2234 schema1 = self._makeTestSchema()
2235 self.butler.put(schema1, self.datasetType, dataId={})
2237 schema2 = self.butler.get(self.datasetType, dataId={})
2238 self.assertEqual(schema2, schema1)
2240 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe schema without pandas.")
2241 def testWriteArrowSchemaReadAsDataFrameSchema(self):
2242 schema1 = self._makeTestSchema()
2243 self.butler.put(schema1, self.datasetType, dataId={})
2245 df_schema1 = DataFrameSchema.from_arrow(schema1)
2247 df_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrameSchema")
2248 self.assertEqual(df_schema2, df_schema1)
2250 @unittest.skipUnless(atable is not None, "Cannot test reading as an astropy schema without astropy.")
2251 def testWriteArrowSchemaReadAsArrowAstropySchema(self):
2252 schema1 = self._makeTestSchema()
2253 self.butler.put(schema1, self.datasetType, dataId={})
2255 ap_schema1 = ArrowAstropySchema.from_arrow(schema1)
2257 ap_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropySchema")
2258 self.assertEqual(ap_schema2, ap_schema1)
2260 # Confirm that the ap_schema2 has the unit/description we expect.
2261 for name in schema1.names:
2262 field_metadata = schema1.field(name).metadata
2263 if field_metadata is None:
2264 continue
2265 if (
2266 b"description" in field_metadata
2267 and (description := field_metadata[b"description"].decode("UTF-8")) != ""
2268 ):
2269 self.assertEqual(ap_schema2.schema[name].description, description)
2270 else:
2271 self.assertIsNone(ap_schema2.schema[name].description)
2272 if b"unit" in field_metadata and (unit := field_metadata[b"unit"].decode("UTF-8")) != "":
2273 self.assertEqual(ap_schema2.schema[name].unit, units.Unit(unit))
2275 @unittest.skipUnless(atable is not None, "Cannot test reading as an numpy schema without numpy.")
2276 def testWriteArrowSchemaReadAsArrowNumpySchema(self):
2277 schema1 = self._makeTestSchema()
2278 self.butler.put(schema1, self.datasetType, dataId={})
2280 np_schema1 = ArrowNumpySchema.from_arrow(schema1)
2282 np_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpySchema")
2283 self.assertEqual(np_schema2, np_schema1)
2286@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with ArrowSchema without pyarrow.")
2287class InMemoryArrowSchemaDelegateTestCase(ParquetFormatterArrowSchemaTestCase):
2288 """Tests for InMemoryDatastore and ArrowSchema."""
2290 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml")
2293@unittest.skipUnless(pa is not None, "Cannot test remote datastore without pyarrow.")
2294@unittest.skipUnless(fsspec is not None, "Cannot test remote datastore without fsspec.")
2295class ParquetFormatterArrowTableRemoteTestCase(unittest.TestCase):
2296 """Tests for arrow table/parquet with a datastore that reports itself as
2297 not local.
2298 """
2300 # Code is adapted from test_butler.py
2301 configFile = os.path.join(TESTDIR, "config/basic/butler-remotetest-store.yaml")
2303 def setUp(self):
2304 config = Config(self.configFile)
2306 self.root = makeTestTempDir(TESTDIR)
2307 # The space in the directory name is deliberate, to exercise URI
2308 # percent-encoding.
2309 root_path = os.path.join(self.root, "butler root")
2310 os.makedirs(root_path)
2311 rooturi = make_remote_test_uri(root_path)
2312 config.update({"datastore": {"datastore": {"root": str(rooturi)}}})
2314 # The registry database has to live on a real local file system.
2315 self.reg_dir = makeTestTempDir(TESTDIR)
2316 config["registry", "db"] = f"sqlite:///{self.reg_dir}/gen3.sqlite3"
2318 Butler.makeRepo(rooturi, config=config, forceConfigRoot=False)
2319 self.tmpConfigFile = str(rooturi.join("butler.yaml", forceDirectory=False))
2321 self.butler = Butler(self.tmpConfigFile, writeable=True, run="test_run")
2322 self.enterContext(self.butler)
2324 # No dimensions in dataset type so we don't have to worry about
2325 # inserting dimension data or defining data IDs.
2326 self.datasetType = DatasetType(
2327 "data", dimensions=(), storageClass="ArrowTable", universe=self.butler.dimensions
2328 )
2329 self.butler.registry.registerDatasetType(self.datasetType)
2331 def tearDown(self):
2332 removeTestTempDir(self.reg_dir)
2333 removeTestTempDir(self.root)
2335 def testArrowTableRemote(self):
2336 tab1 = _makeSimpleArrowTable(include_multidim=True, include_masked=True)
2338 self.butler.put(tab1, self.datasetType, dataId={})
2340 # Read the whole Table.
2341 tab2 = self.butler.get(self.datasetType, dataId={})
2342 # We convert to use the numpy testing framework to handle nan
2343 # comparisons.
2344 self.assertEqual(tab1.schema, tab2.schema)
2345 tab1_np = arrow_to_numpy(tab1)
2346 tab2_np = arrow_to_numpy(tab2)
2347 for col in tab1.column_names:
2348 np.testing.assert_array_equal(tab2_np[col], tab1_np[col])
2349 # Read the columns.
2350 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={})
2351 self.assertEqual(len(columns2), len(tab1.schema.names))
2352 for i, name in enumerate(tab1.schema.names):
2353 self.assertEqual(columns2[i], name)
2354 # Read the rowcount.
2355 rowcount = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={})
2356 self.assertEqual(rowcount, len(tab1))
2357 # Read the schema.
2358 schema = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={})
2359 self.assertEqual(schema, tab1.schema)
2360 # Read just some columns a few different ways.
2361 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]})
2362 self.assertEqual(tab3, tab1.select(("a", "c")))
2363 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"})
2364 self.assertEqual(tab4, tab1.select(("a",)))
2365 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]})
2366 self.assertEqual(tab5, tab1.select(("index", "a")))
2367 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"})
2368 self.assertEqual(tab6, tab1.select(("ddd",)))
2369 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]})
2370 self.assertEqual(tab7, tab1.select(("a",)))
2371 # Passing an unrecognized column should be a ValueError.
2372 with self.assertRaises(ValueError):
2373 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]})
2376@unittest.skipUnless(np is not None, "Cannot test compute_row_group_size without numpy.")
2377@unittest.skipUnless(pa is not None, "Cannot test compute_row_group_size without pyarrow.")
2378class ComputeRowGroupSizeTestCase(unittest.TestCase):
2379 """Tests for compute_row_group_size."""
2381 def testRowGroupSizeNoMetadata(self):
2382 numpyTable = _makeSimpleNumpyTable(include_multidim=True)
2384 # We can't use the numpy_to_arrow convenience function because
2385 # that adds metadata.
2386 type_list = _numpy_dtype_to_arrow_types(numpyTable.dtype)
2387 schema = pa.schema(type_list)
2388 arrays = _numpy_style_arrays_to_arrow_arrays(
2389 numpyTable.dtype,
2390 len(numpyTable),
2391 numpyTable,
2392 schema,
2393 )
2394 arrowTable = pa.Table.from_arrays(arrays, schema=schema)
2396 row_group_size = compute_row_group_size(arrowTable.schema)
2398 self.assertGreater(row_group_size, 1_000_000)
2399 self.assertLess(row_group_size, 2_000_000)
2401 def testRowGroupSizeWithMetadata(self):
2402 numpyTable = _makeSimpleNumpyTable(include_multidim=True)
2404 arrowTable = numpy_to_arrow(numpyTable)
2406 row_group_size = compute_row_group_size(arrowTable.schema)
2408 self.assertGreater(row_group_size, 1_000_000)
2409 self.assertLess(row_group_size, 2_000_000)
2411 def testRowGroupSizeTinyTable(self):
2412 numpyTable = np.zeros(1, dtype=[("a", np.bool_)])
2414 arrowTable = numpy_to_arrow(numpyTable)
2416 row_group_size = compute_row_group_size(arrowTable.schema)
2418 self.assertGreater(row_group_size, 1_000_000)
2420 @unittest.skipUnless(pd is not None, "Cannot run testRowGroupSizeDataFrameWithLists without pandas.")
2421 def testRowGroupSizeDataFrameWithLists(self):
2422 df = pd.DataFrame({"a": np.zeros(10), "b": [[0, 0]] * 10, "c": [[0.0, 0.0]] * 10, "d": [[]] * 10})
2423 arrowTable = pandas_to_arrow(df)
2424 row_group_size = compute_row_group_size(arrowTable.schema)
2426 self.assertGreater(row_group_size, 1_000_000)
2429def _checkAstropyTableEquality(table1, table2, skip_units=False, has_bigendian=False):
2430 """Check if two astropy tables have the same columns/values.
2432 Parameters
2433 ----------
2434 table1 : `astropy.table.Table`
2435 table2 : `astropy.table.Table`
2436 skip_units : `bool`
2437 has_bigendian : `bool`
2438 """
2439 if not has_bigendian:
2440 assert table1.dtype == table2.dtype
2441 else:
2442 for name in table1.dtype.names:
2443 # Only check type matches, force to little-endian.
2444 assert table1.dtype[name].newbyteorder(">") == table2.dtype[name].newbyteorder(">")
2446 # Strip provenance before comparison.
2447 DatasetProvenance.strip_provenance_from_flat_dict(table1.meta)
2448 DatasetProvenance.strip_provenance_from_flat_dict(table2.meta)
2449 assert table1.meta == table2.meta
2450 if not skip_units:
2451 for name in table1.columns:
2452 assert table1[name].unit == table2[name].unit
2453 assert table1[name].description == table2[name].description
2454 assert table1[name].format == table2[name].format
2456 for name in table1.columns:
2457 # We need to check masked/regular columns after filling.
2458 has_masked = False
2459 if isinstance(table1[name], atable.column.MaskedColumn):
2460 c1 = table1[name].filled()
2461 has_masked = True
2462 else:
2463 c1 = np.array(table1[name])
2464 if has_masked:
2465 assert isinstance(table2[name], atable.column.MaskedColumn)
2466 c2 = table2[name].filled()
2467 else:
2468 assert not isinstance(table2[name], atable.column.MaskedColumn)
2469 c2 = np.array(table2[name])
2470 np.testing.assert_array_equal(c1, c2)
2471 # If we have a masked column then we test the underlying data.
2472 if has_masked:
2473 np.testing.assert_array_equal(np.array(c1), np.array(c2))
2474 np.testing.assert_array_equal(table1[name].mask, table2[name].mask)
2477def _checkNumpyTableEquality(table1, table2, has_bigendian=False):
2478 """Check if two numpy tables have the same columns/values
2480 Parameters
2481 ----------
2482 table1 : `numpy.ndarray`
2483 table2 : `numpy.ndarray`
2484 has_bigendian : `bool`
2485 """
2486 assert table1.dtype.names == table2.dtype.names
2487 for name in table1.dtype.names:
2488 if not has_bigendian:
2489 assert table1.dtype[name] == table2.dtype[name]
2490 else:
2491 # Only check type matches, force to little-endian.
2492 assert table1.dtype[name].newbyteorder(">") == table2.dtype[name].newbyteorder(">")
2493 assert np.all(table1 == table2)
2496def _checkNumpyDictEquality(dict1, dict2):
2497 """Check if two numpy dicts have the same columns/values.
2499 Parameters
2500 ----------
2501 dict1 : `dict` [`str`, `np.ndarray`]
2502 dict2 : `dict` [`str`, `np.ndarray`]
2503 """
2504 assert set(dict1.keys()) == set(dict2.keys())
2505 for name in dict1:
2506 assert dict1[name].dtype == dict2[name].dtype
2507 assert np.all(dict1[name] == dict2[name])
2510if __name__ == "__main__":
2511 unittest.main()