Coverage for tests/test_parquet.py: 99%

1306 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-17 20:49 +0000

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28"""Tests for ParquetFormatter. 

29 

30Tests in this module are disabled unless pandas and pyarrow are importable. 

31""" 

32 

33import datetime 

34import os 

35import unittest 

36import uuid 

37 

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 

55 

56try: 

57 import fsspec 

58except ImportError: 

59 fsspec = None 

60 

61 

62from lsst.daf.butler import ( 

63 Butler, 

64 Config, 

65 DatasetProvenance, 

66 DatasetRef, 

67 DatasetType, 

68 FileDataset, 

69 StorageClassConfig, 

70 StorageClassFactory, 

71) 

72 

73try: 

74 from lsst.daf.butler.delegates.arrowtable import ArrowTableDelegate 

75except ImportError: 

76 pa = None 

77 

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 

111 

112TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

113 

114 

115def _makeSimpleNumpyTable(include_multidim=False, include_bigendian=False): 

116 """Make a simple numpy table with random data. 

117 

118 Parameters 

119 ---------- 

120 include_multidim : `bool` 

121 Include multi-dimensional columns. 

122 include_bigendian : `bool` 

123 Include big-endian columns. 

124 

125 Returns 

126 ------- 

127 numpyTable : `numpy.ndarray` 

128 """ 

129 nrow = 5 

130 

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 ] 

143 

144 if include_multidim: 

145 dtype.extend( 

146 [ 

147 ("d1", "f4", (5,)), 

148 ("d2", "i8", (5, 10)), 

149 ("d3", "f8", (5, 10)), 

150 ] 

151 ) 

152 

153 if include_bigendian: 

154 dtype.extend([("a_bigendian", ">f8"), ("f_bigendian", ">i8")]) 

155 

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") 

167 

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)) 

172 

173 if include_bigendian: 

174 data["a_bigendian"][:] = data["a"] 

175 data["f_bigendian"][:] = data["f"] 

176 

177 return data 

178 

179 

180def _makeSingleIndexDataFrame(include_masked=False, include_lists=False): 

181 """Make a single index data frame for testing. 

182 

183 Parameters 

184 ---------- 

185 include_masked : `bool` 

186 Include masked columns. 

187 include_lists : `bool` 

188 Include list columns. 

189 

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") 

200 

201 if include_masked: 

202 nrow = len(df) 

203 

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 

209 

210 if include_lists: 

211 nrow = len(df) 

212 

213 df["l1"] = [[0, 0]] * nrow 

214 df["l2"] = [[0.0, 0.0]] * nrow 

215 df["l3"] = [[]] * nrow 

216 

217 allColumns = df.columns.append(pd.Index(df.index.names)) 

218 

219 return df, allColumns 

220 

221 

222def _makeMultiIndexDataFrame(): 

223 """Make a multi-index data frame for testing. 

224 

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) 

242 

243 return df 

244 

245 

246def _makeSimpleAstropyTable(include_multidim=False, include_masked=False, include_bigendian=False): 

247 """Make an astropy table for testing. 

248 

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. 

257 

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" 

270 

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. 

278 

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"") 

308 

309 return table 

310 

311 

312def _makeSimpleArrowTable(include_multidim=False, include_masked=False): 

313 """Make an arrow table for testing. 

314 

315 Parameters 

316 ---------- 

317 include_multidim : `bool` 

318 Include multi-dimensional columns. 

319 include_masked : `bool` 

320 Include masked columns. 

321 

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) 

329 

330 

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.""" 

335 

336 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml") 

337 

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) 

353 

354 def tearDown(self): 

355 removeTestTempDir(self.root) 

356 

357 def testSingleIndexDataFrame(self): 

358 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True) 

359 

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"]}) 

391 

392 def testSingleIndexDataFrameWithLists(self): 

393 df1, allColumns = _makeSingleIndexDataFrame(include_lists=True) 

394 

395 self.butler.put(df1, self.datasetType, dataId={}) 

396 # Read the whole DataFrame. 

397 df2 = self.butler.get(self.datasetType, dataId={}) 

398 

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])) 

404 

405 def testMultiIndexDataFrame(self): 

406 df1 = _makeMultiIndexDataFrame() 

407 

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"]}) 

438 

439 def testSingleIndexDataFrameEmptyString(self): 

440 """Test persisting a single index dataframe with empty strings.""" 

441 df1, _ = _makeSingleIndexDataFrame() 

442 

443 # Set one of the strings to None 

444 df1.at[1, "strcol"] = None 

445 

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)) 

450 

451 def testSingleIndexDataFrameAllEmptyStrings(self): 

452 """Test persisting a single index dataframe with an empty string 

453 column. 

454 """ 

455 df1, _ = _makeSingleIndexDataFrame() 

456 

457 # Set all of the strings to None 

458 df1.loc[0:, "strcol"] = None 

459 

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)) 

464 

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() 

471 

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) 

479 

480 fname = os.path.join(self.root, "test_dataframe.parq") 

481 df1.to_parquet(fname) 

482 

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) 

490 

491 data_id = {} 

492 ref = DatasetRef(legacy_type, data_id, run=self.run) 

493 dataset = FileDataset(path=fname, refs=[ref], formatter=ParquetFormatter) 

494 

495 self.butler.ingest(dataset, transfer="copy") 

496 

497 self.butler.put(df1, self.datasetType, dataId={}) 

498 

499 df2a = self.butler.get(self.datasetType, dataId={}) 

500 df2b = self.butler.get("legacy_dataframe", dataId={}) 

501 self.assertTrue(df2a.equals(df2b)) 

502 

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)) 

506 

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)) 

510 

511 rowcount2a = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={}) 

512 rowcount2b = self.butler.get("legacy_dataframe.rowcount", dataId={}) 

513 self.assertEqual(rowcount2a, rowcount2b) 

514 

515 schema2a = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={}) 

516 schema2b = self.butler.get("legacy_dataframe.schema", dataId={}) 

517 self.assertEqual(schema2a, schema2b) 

518 

519 def testDataFrameSchema(self): 

520 tab1 = _makeSimpleArrowTable() 

521 

522 schema = DataFrameSchema.from_arrow(tab1.schema) 

523 

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) 

528 

529 tab2 = _makeMultiIndexDataFrame() 

530 schema2 = DataFrameSchema(tab2) 

531 

532 self.assertNotEqual(schema, schema2) 

533 

534 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.") 

535 def testWriteSingleIndexDataFrameReadAsAstropyTable(self): 

536 df1, allColumns = _makeSingleIndexDataFrame() 

537 

538 self.butler.put(df1, self.datasetType, dataId={}) 

539 

540 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy") 

541 

542 tab2_df = tab2.to_pandas(index="index") 

543 self.assertTrue(df1.equals(tab2_df)) 

544 

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)) 

552 

553 # Check reading the schema. 

554 schema = ArrowAstropySchema(tab2) 

555 schema2 = self.butler.get( 

556 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema" 

557 ) 

558 

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) 

566 

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) 

574 

575 self.butler.put(df1, self.datasetType, dataId={}) 

576 

577 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy") 

578 tab2_df = astropy_to_pandas(tab2, index="index") 

579 

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] 

584 

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)) 

594 

595 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.") 

596 def testWriteMultiIndexDataFrameReadAsAstropyTable(self): 

597 df1 = _makeMultiIndexDataFrame() 

598 

599 self.butler.put(df1, self.datasetType, dataId={}) 

600 

601 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy") 

602 

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. 

606 

607 @unittest.skipUnless(atable is not None, "Cannot test writing as astropy without astropy.") 

608 def testWriteAstropyTableWithMaskedColsReadAsSingleIndexDataFrame(self): 

609 tab1 = _makeSimpleAstropyTable(include_masked=True) 

610 

611 self.butler.put(tab1, self.datasetType, dataId={}) 

612 

613 tab2 = self.butler.get(self.datasetType, dataId={}) 

614 

615 tab1_df = astropy_to_pandas(tab1) 

616 self.assertTrue(tab1_df.equals(tab2)) 

617 

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) 

623 

624 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.") 

625 def testWriteSingleIndexDataFrameReadAsArrowTable(self): 

626 df1, allColumns = _makeSingleIndexDataFrame() 

627 

628 self.butler.put(df1, self.datasetType, dataId={}) 

629 

630 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable") 

631 

632 tab2_df = arrow_to_pandas(tab2) 

633 self.assertTrue(df1.equals(tab2_df)) 

634 

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)) 

642 

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)) 

648 

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)) 

656 

657 # Check reading the schema. 

658 schema = tab2.schema 

659 schema2 = self.butler.get( 

660 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema" 

661 ) 

662 

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) 

669 

670 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.") 

671 def testWriteMultiIndexDataFrameReadAsArrowTable(self): 

672 df1 = _makeMultiIndexDataFrame() 

673 

674 self.butler.put(df1, self.datasetType, dataId={}) 

675 

676 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable") 

677 

678 tab2_df = arrow_to_pandas(tab2) 

679 self.assertTrue(df1.equals(tab2_df)) 

680 

681 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.") 

682 def testWriteSingleIndexDataFrameReadAsNumpyTable(self): 

683 df1, allColumns = _makeSingleIndexDataFrame() 

684 

685 self.butler.put(df1, self.datasetType, dataId={}) 

686 

687 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy") 

688 

689 tab2_df = pd.DataFrame.from_records(tab2, index=["index"]) 

690 self.assertTrue(df1.equals(tab2_df)) 

691 

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)) 

699 

700 # Check reading the schema. 

701 schema = ArrowNumpySchema(tab2.dtype) 

702 schema2 = self.butler.get( 

703 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema" 

704 ) 

705 

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) 

717 

718 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.") 

719 def testWriteMultiIndexDataFrameReadAsNumpyTable(self): 

720 df1 = _makeMultiIndexDataFrame() 

721 

722 self.butler.put(df1, self.datasetType, dataId={}) 

723 

724 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy") 

725 

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. 

729 

730 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.") 

731 def testWriteSingleIndexDataFrameReadAsNumpyDict(self): 

732 df1, allColumns = _makeSingleIndexDataFrame() 

733 

734 self.butler.put(df1, self.datasetType, dataId={}) 

735 

736 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict") 

737 

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)) 

743 

744 @unittest.skipUnless(np is not None, "Cannot test reading as numpy dict without numpy.") 

745 def testWriteMultiIndexDataFrameReadAsNumpyDict(self): 

746 df1 = _makeMultiIndexDataFrame() 

747 

748 self.butler.put(df1, self.datasetType, dataId={}) 

749 

750 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict") 

751 

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. 

755 

756 def testBadDataFrameColumnParquet(self): 

757 df1, allColumns = _makeSingleIndexDataFrame() 

758 

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 

764 

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={}) 

769 

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) 

773 

774 put_ref = self.butler.put(tab1, self.datasetType, dataId={}) 

775 

776 tab2 = self.butler.get( 

777 self.datasetType, 

778 dataId={}, 

779 storageClass="ArrowAstropy", 

780 parameters={"strip_astropy_meta_yaml": False}, 

781 ) 

782 

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 } 

790 

791 self.assertEqual(tab2.meta, expected) 

792 

793 _checkAstropyTableEquality(tab1, tab2) 

794 

795 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.") 

796 def testWriteReadAstropyTableProvenance(self): 

797 tab1 = _makeSimpleAstropyTable() 

798 

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) 

811 

812 put_ref = self.butler.put(tab1, self.datasetType, dataId={}, provenance=provenance) 

813 

814 tab2 = self.butler.get( 

815 self.datasetType, 

816 dataId={}, 

817 storageClass="ArrowAstropy", 

818 parameters={"strip_astropy_meta_yaml": False}, 

819 ) 

820 

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) 

832 

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") 

837 

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()) 

849 

850 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.") 

851 def testWriteReadNumpyTableLossless(self): 

852 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

853 

854 self.butler.put(tab1, self.datasetType, dataId={}) 

855 

856 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy") 

857 

858 _checkNumpyTableEquality(tab1, tab2) 

859 

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}") 

868 

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) 

873 

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) 

877 

878 self.butler.put(tab1, self.datasetType, dataId={}) 

879 

880 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable") 

881 

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]) 

887 

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) 

892 

893 self.butler.put(tab1, self.datasetType, dataId={}) 

894 

895 dict2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict") 

896 

897 _checkNumpyDictEquality(dict1, dict2) 

898 

899 

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.""" 

903 

904 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml") 

905 

906 def testBadDataFrameColumnParquet(self): 

907 # This test does not raise for an in-memory datastore. 

908 pass 

909 

910 def testWriteMultiIndexDataFrameReadAsAstropyTable(self): 

911 df1 = _makeMultiIndexDataFrame() 

912 

913 self.butler.put(df1, self.datasetType, dataId={}) 

914 

915 with self.assertRaises(ValueError): 

916 _ = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy") 

917 

918 def testLegacyDataFrame(self): 

919 # This test does not work with an inMemoryDatastore. 

920 pass 

921 

922 def testBadInput(self): 

923 df1, _ = _makeSingleIndexDataFrame() 

924 delegate = ArrowTableDelegate("DataFrame") 

925 

926 with self.assertRaises(ValueError): 

927 delegate.handleParameters(inMemoryDataset="not_a_dataframe") 

928 

929 with self.assertRaises(AttributeError): 

930 delegate.getComponent(composite=df1, componentName="nothing") 

931 

932 def testStorageClass(self): 

933 df1, allColumns = _makeSingleIndexDataFrame() 

934 

935 factory = StorageClassFactory() 

936 factory.addFromConfig(StorageClassConfig()) 

937 

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") 

942 

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") 

947 

948 

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.""" 

953 

954 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml") 

955 

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) 

971 

972 def tearDown(self): 

973 removeTestTempDir(self.root) 

974 

975 def testAstropyTable(self): 

976 tab1 = _makeSimpleAstropyTable(include_multidim=True, include_masked=True) 

977 

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"]}) 

1011 

1012 def testAstropyTableBigEndian(self): 

1013 tab1 = _makeSimpleAstropyTable(include_bigendian=True) 

1014 

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) 

1019 

1020 def testAstropyTableWithMetadata(self): 

1021 tab1 = _makeSimpleAstropyTable(include_multidim=True) 

1022 

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 } 

1030 

1031 tab1.meta.update(meta) 

1032 

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) 

1038 

1039 def testArrowAstropySchema(self): 

1040 tab1 = _makeSimpleAstropyTable() 

1041 tab1_arrow = astropy_to_arrow(tab1) 

1042 schema = ArrowAstropySchema.from_arrow(tab1_arrow.schema) 

1043 

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) 

1048 

1049 # Test various inequalities 

1050 tab2 = tab1.copy() 

1051 tab2.rename_column("index", "index2") 

1052 schema2 = ArrowAstropySchema(tab2) 

1053 self.assertNotEqual(schema2, schema) 

1054 

1055 tab2 = tab1.copy() 

1056 tab2["index"].unit = units.micron 

1057 schema2 = ArrowAstropySchema(tab2) 

1058 self.assertNotEqual(schema2, schema) 

1059 

1060 tab2 = tab1.copy() 

1061 tab2["index"].description = "Index column" 

1062 schema2 = ArrowAstropySchema(tab2) 

1063 self.assertNotEqual(schema2, schema) 

1064 

1065 tab2 = tab1.copy() 

1066 tab2["index"].format = "%05d" 

1067 schema2 = ArrowAstropySchema(tab2) 

1068 self.assertNotEqual(schema2, schema) 

1069 

1070 def testAstropyParquet(self): 

1071 tab1 = _makeSimpleAstropyTable() 

1072 

1073 # Remove datetime column which doesn't work with astropy currently. 

1074 del tab1["dtn"] 

1075 del tab1["dtu"] 

1076 

1077 fname = os.path.join(self.root, "test_astropy.parq") 

1078 tab1.write(fname) 

1079 

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) 

1087 

1088 data_id = {} 

1089 ref = DatasetRef(astropy_type, data_id, run=self.run) 

1090 dataset = FileDataset(path=fname, refs=[ref], formatter=ParquetFormatter) 

1091 

1092 self.butler.ingest(dataset, transfer="copy") 

1093 

1094 self.butler.put(tab1, self.datasetType, dataId={}) 

1095 

1096 tab2a = self.butler.get(self.datasetType, dataId={}) 

1097 tab2b = self.butler.get("astropy_parquet", dataId={}) 

1098 _checkAstropyTableEquality(tab2a, tab2b) 

1099 

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) 

1105 

1106 rowcount2a = self.butler.get(self.datasetType.componentTypeName("rowcount"), dataId={}) 

1107 rowcount2b = self.butler.get("astropy_parquet.rowcount", dataId={}) 

1108 self.assertEqual(rowcount2a, rowcount2b) 

1109 

1110 schema2a = self.butler.get(self.datasetType.componentTypeName("schema"), dataId={}) 

1111 schema2b = self.butler.get("astropy_parquet.schema", dataId={}) 

1112 self.assertEqual(schema2a, schema2b) 

1113 

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) 

1118 

1119 self.butler.put(tab1, self.datasetType, dataId={}) 

1120 

1121 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable") 

1122 

1123 tab2_astropy = arrow_to_astropy(tab2) 

1124 _checkAstropyTableEquality(tab1, tab2_astropy) 

1125 

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) 

1132 

1133 # Check reading the schema. 

1134 schema = tab2.schema 

1135 schema2 = self.butler.get( 

1136 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowSchema" 

1137 ) 

1138 

1139 self.assertEqual(schema, schema2) 

1140 

1141 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.") 

1142 def testWriteAstropyReadAsDataFrame(self): 

1143 tab1 = _makeSimpleAstropyTable() 

1144 

1145 self.butler.put(tab1, self.datasetType, dataId={}) 

1146 

1147 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame") 

1148 

1149 # This is tricky because it loses the units and gains a bonus pandas 

1150 # _index_ column, so we just test the dataframe form. 

1151 

1152 tab1_df = tab1.to_pandas() 

1153 self.assertTrue(tab1_df.equals(tab2)) 

1154 

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)) 

1161 

1162 # Check reading the schema. 

1163 schema = DataFrameSchema(tab2) 

1164 schema2 = self.butler.get( 

1165 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema" 

1166 ) 

1167 

1168 self.assertEqual(schema2, schema) 

1169 

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) 

1177 

1178 self.butler.put(tab1, self.datasetType, dataId={}) 

1179 

1180 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame") 

1181 

1182 tab1_df = astropy_to_pandas(tab1) 

1183 

1184 self.assertTrue(tab1_df.columns.equals(tab2.columns)) 

1185 for name in tab2.columns: 

1186 col1 = tab1_df[name] 

1187 col2 = tab2[name] 

1188 

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)) 

1198 

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) 

1202 

1203 self.butler.put(df1, self.datasetType, dataId={}) 

1204 

1205 tab2 = self.butler.get(self.datasetType, dataId={}) 

1206 

1207 df1_tab = pandas_to_astropy(df1) 

1208 

1209 _checkAstropyTableEquality(df1_tab, tab2) 

1210 

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={}) 

1215 

1216 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy") 

1217 

1218 # This is tricky because it loses the units. 

1219 tab2_astropy = atable.Table(tab2) 

1220 

1221 _checkAstropyTableEquality(tab1, tab2_astropy, skip_units=True) 

1222 

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) 

1229 

1230 # Check reading the schema. 

1231 schema = ArrowNumpySchema(tab2.dtype) 

1232 schema2 = self.butler.get( 

1233 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowNumpySchema" 

1234 ) 

1235 

1236 self.assertEqual(schema2, schema) 

1237 

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={}) 

1242 

1243 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict") 

1244 

1245 # This is tricky because it loses the units. 

1246 tab2_astropy = atable.Table(tab2) 

1247 

1248 _checkAstropyTableEquality(tab1, tab2_astropy, skip_units=True) 

1249 

1250 def testBadAstropyColumnParquet(self): 

1251 tab1 = _makeSimpleAstropyTable() 

1252 

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 

1258 

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={}) 

1263 

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 

1269 

1270 with self.assertRaises(RuntimeError): 

1271 self.butler.put(bad_tab, self.datasetType, dataId={}) 

1272 

1273 @unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.") 

1274 def testWriteAstropyTableWithPandasIndexHint(self, testStrip=True): 

1275 tab1 = _makeSimpleAstropyTable() 

1276 

1277 add_pandas_index_to_astropy(tab1, "index") 

1278 

1279 self.butler.put(tab1, self.datasetType, dataId={}) 

1280 

1281 # Read in as an astropy table and ensure index hint is still there. 

1282 tab2 = self.butler.get(self.datasetType, dataId={}) 

1283 

1284 self.assertIn(ASTROPY_PANDAS_INDEX_KEY, tab2.meta) 

1285 self.assertEqual(tab2.meta[ASTROPY_PANDAS_INDEX_KEY], "index") 

1286 

1287 # Read as a dataframe and ensure index is set. 

1288 df3 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame") 

1289 

1290 self.assertEqual(df3.index.name, "index") 

1291 

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]) 

1301 

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"]}) 

1305 

1306 self.assertNotIn(ASTROPY_PANDAS_INDEX_KEY, tab5.meta) 

1307 

1308 with self.assertRaises(ValueError): 

1309 add_pandas_index_to_astropy(tab1, "not_a_column") 

1310 

1311 

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 """ 

1317 

1318 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml") 

1319 

1320 def testAstropyParquet(self): 

1321 # This test does not work with an inMemoryDatastore. 

1322 pass 

1323 

1324 def testBadAstropyColumnParquet(self): 

1325 # This test does not raise for an in-memory datastore. 

1326 pass 

1327 

1328 def testBadInput(self): 

1329 tab1 = _makeSimpleAstropyTable() 

1330 delegate = ArrowTableDelegate("ArrowAstropy") 

1331 

1332 with self.assertRaises(ValueError): 

1333 delegate.handleParameters(inMemoryDataset="not_an_astropy_table") 

1334 

1335 with self.assertRaises(NotImplementedError): 

1336 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]}) 

1337 

1338 with self.assertRaises(AttributeError): 

1339 delegate.getComponent(composite=tab1, componentName="nothing") 

1340 

1341 @unittest.skipUnless(pd is not None, "Cannot test ParquetFormatterDataFrame without pandas.") 

1342 def testWriteAstropyTableWithPandasIndexHint(self): 

1343 super().testWriteAstropyTableWithPandasIndexHint(testStrip=False) 

1344 

1345 

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.""" 

1350 

1351 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml") 

1352 

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) 

1367 

1368 def tearDown(self): 

1369 removeTestTempDir(self.root) 

1370 

1371 def testNumpyTable(self): 

1372 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1373 

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"]}) 

1436 

1437 def testNumpyTableBigEndian(self): 

1438 tab1 = _makeSimpleNumpyTable(include_bigendian=True) 

1439 

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) 

1444 

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) 

1449 

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) 

1454 

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) 

1462 

1463 @unittest.skipUnless(pa is not None, "Cannot test arrow conversions without pyarrow.") 

1464 def testNumpyDictConversions(self): 

1465 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1466 

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) 

1471 

1472 self.assertEqual(tab1_arrow.schema, tab1_dict_arrow.schema) 

1473 self.assertEqual(tab1_arrow, tab1_dict_arrow) 

1474 

1475 @unittest.skipUnless(pa is not None, "Cannot test reading as arrow without pyarrow.") 

1476 def testWriteNumpyTableReadAsArrowTable(self): 

1477 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1478 

1479 self.butler.put(tab1, self.datasetType, dataId={}) 

1480 

1481 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable") 

1482 

1483 tab2_numpy = arrow_to_numpy(tab2) 

1484 

1485 _checkNumpyTableEquality(tab1, tab2_numpy) 

1486 

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) 

1493 

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) 

1500 

1501 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.") 

1502 def testWriteNumpyTableReadAsDataFrame(self): 

1503 tab1 = _makeSimpleNumpyTable() 

1504 

1505 self.butler.put(tab1, self.datasetType, dataId={}) 

1506 

1507 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame") 

1508 

1509 # Converting this back to numpy gets confused with the index column 

1510 # and changes the datatype of the string column. 

1511 

1512 tab1_df = pd.DataFrame(tab1) 

1513 

1514 self.assertTrue(tab1_df.equals(tab2)) 

1515 

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)) 

1522 

1523 # Check reading the schema. 

1524 schema = DataFrameSchema(tab2) 

1525 schema2 = self.butler.get( 

1526 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="DataFrameSchema" 

1527 ) 

1528 

1529 self.assertEqual(schema2, schema) 

1530 

1531 @unittest.skipUnless(atable is not None, "Cannot test reading as astropy without astropy.") 

1532 def testWriteNumpyTableReadAsAstropyTable(self): 

1533 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1534 

1535 self.butler.put(tab1, self.datasetType, dataId={}) 

1536 

1537 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy") 

1538 tab2_numpy = tab2.as_array() 

1539 

1540 _checkNumpyTableEquality(tab1, tab2_numpy) 

1541 

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) 

1548 

1549 # Check reading the schema. 

1550 schema = ArrowAstropySchema(tab2) 

1551 schema2 = self.butler.get( 

1552 self.datasetType.componentTypeName("schema"), dataId={}, storageClass="ArrowAstropySchema" 

1553 ) 

1554 

1555 self.assertEqual(schema2, schema) 

1556 

1557 def testWriteNumpyTableReadAsNumpyDict(self): 

1558 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1559 

1560 self.butler.put(tab1, self.datasetType, dataId={}) 

1561 

1562 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict") 

1563 tab2_numpy = _numpy_dict_to_numpy(tab2) 

1564 

1565 _checkNumpyTableEquality(tab1, tab2_numpy) 

1566 

1567 def testBadNumpyColumnParquet(self): 

1568 tab1 = _makeSimpleAstropyTable() 

1569 

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 

1575 

1576 bad_tab_np = bad_tab.as_array() 

1577 

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={}) 

1582 

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 

1588 

1589 bad_tab_np = bad_tab.as_array() 

1590 

1591 with self.assertRaises(RuntimeError): 

1592 self.butler.put(bad_tab_np, self.datasetType, dataId={}) 

1593 

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) 

1597 

1598 self.butler.put(tab1, self.datasetType, dataId={}) 

1599 

1600 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy") 

1601 

1602 _checkAstropyTableEquality(tab1, tab2) 

1603 

1604 

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 """ 

1610 

1611 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml") 

1612 

1613 def testBadNumpyColumnParquet(self): 

1614 # This test does not raise for an in-memory datastore. 

1615 pass 

1616 

1617 def testBadInput(self): 

1618 tab1 = _makeSimpleNumpyTable() 

1619 delegate = ArrowTableDelegate("ArrowNumpy") 

1620 

1621 with self.assertRaises(ValueError): 

1622 delegate.handleParameters(inMemoryDataset="not_a_numpy_table") 

1623 

1624 with self.assertRaises(NotImplementedError): 

1625 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]}) 

1626 

1627 with self.assertRaises(AttributeError): 

1628 delegate.getComponent(composite=tab1, componentName="nothing") 

1629 

1630 def testStorageClass(self): 

1631 tab1 = _makeSimpleNumpyTable() 

1632 

1633 factory = StorageClassFactory() 

1634 factory.addFromConfig(StorageClassConfig()) 

1635 

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") 

1640 

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") 

1645 

1646 

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.""" 

1650 

1651 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml") 

1652 

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) 

1667 

1668 def tearDown(self): 

1669 removeTestTempDir(self.root) 

1670 

1671 def testArrowTable(self): 

1672 tab1 = _makeSimpleArrowTable(include_multidim=True, include_masked=True) 

1673 

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"]}) 

1711 

1712 def testEmptyArrowTable(self): 

1713 data = _makeSimpleNumpyTable() 

1714 type_list = _numpy_dtype_to_arrow_types(data.dtype) 

1715 

1716 schema = pa.schema(type_list) 

1717 arrays = [[]] * len(schema.names) 

1718 

1719 tab1 = pa.Table.from_arrays(arrays, schema=schema) 

1720 

1721 self.butler.put(tab1, self.datasetType, dataId={}) 

1722 tab2 = self.butler.get(self.datasetType, dataId={}) 

1723 self.assertEqual(tab2, tab1) 

1724 

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) 

1729 

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 ) 

1739 

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) 

1744 

1745 def testEmptyArrowTableMultidim(self): 

1746 data = _makeSimpleNumpyTable(include_multidim=True) 

1747 type_list = _numpy_dtype_to_arrow_types(data.dtype) 

1748 

1749 md = {} 

1750 for name in data.dtype.names: 

1751 _append_numpy_multidim_metadata(md, name, data.dtype[name]) 

1752 

1753 schema = pa.schema(type_list, metadata=md) 

1754 arrays = [[]] * len(schema.names) 

1755 

1756 tab1 = pa.Table.from_arrays(arrays, schema=schema) 

1757 

1758 self.butler.put(tab1, self.datasetType, dataId={}) 

1759 tab2 = self.butler.get(self.datasetType, dataId={}) 

1760 self.assertEqual(tab2, tab1) 

1761 

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) 

1766 

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) 

1771 

1772 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.") 

1773 def testWriteArrowTableReadAsSingleIndexDataFrame(self): 

1774 df1, allColumns = _makeSingleIndexDataFrame() 

1775 

1776 self.butler.put(df1, self.datasetType, dataId={}) 

1777 

1778 # Read back out as a dataframe. 

1779 df2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame") 

1780 self.assertTrue(df1.equals(df2)) 

1781 

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)) 

1786 

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())) 

1794 

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) 

1801 

1802 @unittest.skipUnless(pd is not None, "Cannot test reading as a dataframe without pandas.") 

1803 def testWriteArrowTableReadAsMultiIndexDataFrame(self): 

1804 df1 = _makeMultiIndexDataFrame() 

1805 

1806 self.butler.put(df1, self.datasetType, dataId={}) 

1807 

1808 # Read back out as a dataframe. 

1809 df2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame") 

1810 self.assertTrue(df1.equals(df2)) 

1811 

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)) 

1816 

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)) 

1823 

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) 

1830 

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) 

1834 

1835 self.butler.put(tab1, self.datasetType, dataId={}) 

1836 

1837 # Read back out as an astropy table. 

1838 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy") 

1839 _checkAstropyTableEquality(tab1, tab2) 

1840 

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) 

1845 

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) 

1852 

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) 

1859 

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)) 

1873 

1874 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.") 

1875 def testWriteArrowTableReadAsNumpyTable(self): 

1876 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1877 

1878 self.butler.put(tab1, self.datasetType, dataId={}) 

1879 

1880 # Read back out as a numpy table. 

1881 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy") 

1882 _checkNumpyTableEquality(tab1, tab2) 

1883 

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) 

1888 

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) 

1895 

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) 

1902 

1903 @unittest.skipUnless(np is not None, "Cannot test reading as numpy without numpy.") 

1904 def testWriteArrowTableReadAsNumpyDict(self): 

1905 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1906 

1907 self.butler.put(tab1, self.datasetType, dataId={}) 

1908 

1909 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpyDict") 

1910 tab2_numpy = _numpy_dict_to_numpy(tab2) 

1911 _checkNumpyTableEquality(tab1, tab2_numpy) 

1912 

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) 

1916 

1917 self.butler.put(tab1, self.datasetType, dataId={}) 

1918 

1919 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy") 

1920 

1921 _checkAstropyTableEquality(tab1, tab2) 

1922 

1923 

1924@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with ArroWTable without pyarrow.") 

1925class InMemoryArrowTableDelegateTestCase(ParquetFormatterArrowTableTestCase): 

1926 """Tests for InMemoryDatastore, using ArrowTableDelegate.""" 

1927 

1928 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml") 

1929 

1930 def testBadInput(self): 

1931 tab1 = _makeSimpleArrowTable() 

1932 delegate = ArrowTableDelegate("ArrowTable") 

1933 

1934 with self.assertRaises(ValueError): 

1935 delegate.handleParameters(inMemoryDataset="not_an_arrow_table") 

1936 

1937 with self.assertRaises(NotImplementedError): 

1938 delegate.handleParameters(inMemoryDataset=tab1, parameters={"columns": [("a", "b")]}) 

1939 

1940 with self.assertRaises(AttributeError): 

1941 delegate.getComponent(composite=tab1, componentName="nothing") 

1942 

1943 def testStorageClass(self): 

1944 tab1 = _makeSimpleArrowTable() 

1945 

1946 factory = StorageClassFactory() 

1947 factory.addFromConfig(StorageClassConfig()) 

1948 

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") 

1953 

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") 

1958 

1959 

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.""" 

1964 

1965 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml") 

1966 

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) 

1981 

1982 def tearDown(self): 

1983 removeTestTempDir(self.root) 

1984 

1985 def testNumpyDict(self): 

1986 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1987 dict1 = _numpy_to_numpy_dict(tab1) 

1988 

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"]}) 

2026 

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) 

2031 

2032 self.butler.put(dict1, self.datasetType, dataId={}) 

2033 

2034 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowTable") 

2035 

2036 tab2_dict = arrow_to_numpy_dict(tab2) 

2037 

2038 _checkNumpyDictEquality(dict1, tab2_dict) 

2039 

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) 

2044 

2045 self.butler.put(dict1, self.datasetType, dataId={}) 

2046 

2047 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrame") 

2048 

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) 

2053 

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)) 

2057 

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) 

2062 

2063 self.butler.put(dict1, self.datasetType, dataId={}) 

2064 

2065 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy") 

2066 tab2_dict = _astropy_to_numpy_dict(tab2) 

2067 

2068 _checkNumpyDictEquality(dict1, tab2_dict) 

2069 

2070 def testWriteNumpyDictReadAsNumpyTable(self): 

2071 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

2072 dict1 = _numpy_to_numpy_dict(tab1) 

2073 

2074 self.butler.put(dict1, self.datasetType, dataId={}) 

2075 

2076 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpy") 

2077 tab2_dict = _numpy_to_numpy_dict(tab2) 

2078 

2079 _checkNumpyDictEquality(dict1, tab2_dict) 

2080 

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={}) 

2085 

2086 dict2 = {"a": np.zeros(4), "b": np.zeros(5)} 

2087 with self.assertRaises(RuntimeError): 

2088 self.butler.put(dict2, self.datasetType, dataId={}) 

2089 

2090 dict3 = {"a": [0] * 5, "b": np.zeros(5)} 

2091 with self.assertRaises(RuntimeError): 

2092 self.butler.put(dict3, self.datasetType, dataId={}) 

2093 

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={}) 

2097 

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) 

2101 

2102 self.butler.put(tab1, self.datasetType, dataId={}) 

2103 

2104 tab2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropy") 

2105 

2106 _checkAstropyTableEquality(tab1, tab2) 

2107 

2108 

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 """ 

2115 

2116 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml") 

2117 

2118 def testWriteNumpyDictBad(self): 

2119 # The sub-type checking is not done on in-memory datastore. 

2120 pass 

2121 

2122 

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.""" 

2126 

2127 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml") 

2128 

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) 

2143 

2144 def tearDown(self): 

2145 removeTestTempDir(self.root) 

2146 

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 ) 

2230 

2231 return schema 

2232 

2233 def testArrowSchema(self): 

2234 schema1 = self._makeTestSchema() 

2235 self.butler.put(schema1, self.datasetType, dataId={}) 

2236 

2237 schema2 = self.butler.get(self.datasetType, dataId={}) 

2238 self.assertEqual(schema2, schema1) 

2239 

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={}) 

2244 

2245 df_schema1 = DataFrameSchema.from_arrow(schema1) 

2246 

2247 df_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="DataFrameSchema") 

2248 self.assertEqual(df_schema2, df_schema1) 

2249 

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={}) 

2254 

2255 ap_schema1 = ArrowAstropySchema.from_arrow(schema1) 

2256 

2257 ap_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowAstropySchema") 

2258 self.assertEqual(ap_schema2, ap_schema1) 

2259 

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)) 

2274 

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={}) 

2279 

2280 np_schema1 = ArrowNumpySchema.from_arrow(schema1) 

2281 

2282 np_schema2 = self.butler.get(self.datasetType, dataId={}, storageClass="ArrowNumpySchema") 

2283 self.assertEqual(np_schema2, np_schema1) 

2284 

2285 

2286@unittest.skipUnless(pa is not None, "Cannot test InMemoryDatastore with ArrowSchema without pyarrow.") 

2287class InMemoryArrowSchemaDelegateTestCase(ParquetFormatterArrowSchemaTestCase): 

2288 """Tests for InMemoryDatastore and ArrowSchema.""" 

2289 

2290 configFile = os.path.join(TESTDIR, "config/basic/butler-inmemory.yaml") 

2291 

2292 

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 """ 

2299 

2300 # Code is adapted from test_butler.py 

2301 configFile = os.path.join(TESTDIR, "config/basic/butler-remotetest-store.yaml") 

2302 

2303 def setUp(self): 

2304 config = Config(self.configFile) 

2305 

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)}}}) 

2313 

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" 

2317 

2318 Butler.makeRepo(rooturi, config=config, forceConfigRoot=False) 

2319 self.tmpConfigFile = str(rooturi.join("butler.yaml", forceDirectory=False)) 

2320 

2321 self.butler = Butler(self.tmpConfigFile, writeable=True, run="test_run") 

2322 self.enterContext(self.butler) 

2323 

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) 

2330 

2331 def tearDown(self): 

2332 removeTestTempDir(self.reg_dir) 

2333 removeTestTempDir(self.root) 

2334 

2335 def testArrowTableRemote(self): 

2336 tab1 = _makeSimpleArrowTable(include_multidim=True, include_masked=True) 

2337 

2338 self.butler.put(tab1, self.datasetType, dataId={}) 

2339 

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"]}) 

2374 

2375 

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.""" 

2380 

2381 def testRowGroupSizeNoMetadata(self): 

2382 numpyTable = _makeSimpleNumpyTable(include_multidim=True) 

2383 

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) 

2395 

2396 row_group_size = compute_row_group_size(arrowTable.schema) 

2397 

2398 self.assertGreater(row_group_size, 1_000_000) 

2399 self.assertLess(row_group_size, 2_000_000) 

2400 

2401 def testRowGroupSizeWithMetadata(self): 

2402 numpyTable = _makeSimpleNumpyTable(include_multidim=True) 

2403 

2404 arrowTable = numpy_to_arrow(numpyTable) 

2405 

2406 row_group_size = compute_row_group_size(arrowTable.schema) 

2407 

2408 self.assertGreater(row_group_size, 1_000_000) 

2409 self.assertLess(row_group_size, 2_000_000) 

2410 

2411 def testRowGroupSizeTinyTable(self): 

2412 numpyTable = np.zeros(1, dtype=[("a", np.bool_)]) 

2413 

2414 arrowTable = numpy_to_arrow(numpyTable) 

2415 

2416 row_group_size = compute_row_group_size(arrowTable.schema) 

2417 

2418 self.assertGreater(row_group_size, 1_000_000) 

2419 

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) 

2425 

2426 self.assertGreater(row_group_size, 1_000_000) 

2427 

2428 

2429def _checkAstropyTableEquality(table1, table2, skip_units=False, has_bigendian=False): 

2430 """Check if two astropy tables have the same columns/values. 

2431 

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(">") 

2445 

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 

2455 

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) 

2475 

2476 

2477def _checkNumpyTableEquality(table1, table2, has_bigendian=False): 

2478 """Check if two numpy tables have the same columns/values 

2479 

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) 

2494 

2495 

2496def _checkNumpyDictEquality(dict1, dict2): 

2497 """Check if two numpy dicts have the same columns/values. 

2498 

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]) 

2508 

2509 

2510if __name__ == "__main__": 

2511 unittest.main()