Coverage for tests/test_parquet.py: 99%

1307 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-02 09:06 +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._repo_template_cache import make_repo_for_test 

110from lsst.daf.butler.tests.utils import makeTestTempDir, removeTestTempDir 

111from lsst.resources.tests import make_remote_test_uri 

112 

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

114 

115 

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

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

118 

119 Parameters 

120 ---------- 

121 include_multidim : `bool` 

122 Include multi-dimensional columns. 

123 include_bigendian : `bool` 

124 Include big-endian columns. 

125 

126 Returns 

127 ------- 

128 numpyTable : `numpy.ndarray` 

129 """ 

130 nrow = 5 

131 

132 dtype = [ 

133 ("index", "i4"), 

134 ("a", "f8"), 

135 ("b", "f8"), 

136 ("c", "f8"), 

137 ("ddd", "f8"), 

138 ("f", "i8"), 

139 ("strcol", "U10"), 

140 ("bytecol", "S10"), 

141 ("dtn", "datetime64[ns]"), 

142 ("dtu", "datetime64[us]"), 

143 ] 

144 

145 if include_multidim: 

146 dtype.extend( 

147 [ 

148 ("d1", "f4", (5,)), 

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

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

151 ] 

152 ) 

153 

154 if include_bigendian: 

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

156 

157 data = np.zeros(nrow, dtype=dtype) 

158 data["index"][:] = np.arange(nrow) 

159 data["a"] = np.random.randn(nrow) 

160 data["b"] = np.random.randn(nrow) 

161 data["c"] = np.random.randn(nrow) 

162 data["ddd"] = np.random.randn(nrow) 

163 data["f"] = np.arange(nrow) * 10 

164 data["strcol"][:] = "teststring" 

165 data["bytecol"][:] = "teststring" 

166 data["dtn"] = datetime.datetime.fromisoformat("2024-07-23") 

167 data["dtu"] = datetime.datetime.fromisoformat("2024-07-23") 

168 

169 if include_multidim: 

170 data["d1"] = np.random.randn(data["d1"].size).reshape(data["d1"].shape) 

171 data["d2"] = np.arange(data["d2"].size).reshape(data["d2"].shape) 

172 data["d3"] = np.asfortranarray(np.random.randn(data["d3"].size).reshape(data["d3"].shape)) 

173 

174 if include_bigendian: 

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

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

177 

178 return data 

179 

180 

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

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

183 

184 Parameters 

185 ---------- 

186 include_masked : `bool` 

187 Include masked columns. 

188 include_lists : `bool` 

189 Include list columns. 

190 

191 Returns 

192 ------- 

193 dataFrame : `~pandas.DataFrame` 

194 The test dataframe. 

195 allColumns : `list` [`str`] 

196 List of all the columns (including index columns). 

197 """ 

198 data = _makeSimpleNumpyTable() 

199 df = pd.DataFrame(data) 

200 df = df.set_index("index") 

201 

202 if include_masked: 

203 nrow = len(df) 

204 

205 df["m1"] = pd.array(np.arange(nrow), dtype=pd.Int64Dtype()) 

206 df["m2"] = pd.array(np.arange(nrow), dtype=np.float32) 

207 df["mstrcol"] = pd.array(np.array(["text"] * nrow)) 

208 df.loc[1, ["m1", "m2", "mstrcol"]] = None 

209 df.loc[0, "m1"] = 1649900760361600113 

210 

211 if include_lists: 

212 nrow = len(df) 

213 

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

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

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

217 

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

219 

220 return df, allColumns 

221 

222 

223def _makeMultiIndexDataFrame(): 

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

225 

226 Returns 

227 ------- 

228 dataFrame : `~pandas.DataFrame` 

229 The test dataframe. 

230 """ 

231 columns = pd.MultiIndex.from_tuples( 

232 [ 

233 ("g", "a"), 

234 ("g", "b"), 

235 ("g", "c"), 

236 ("r", "a"), 

237 ("r", "b"), 

238 ("r", "c"), 

239 ], 

240 names=["filter", "column"], 

241 ) 

242 df = pd.DataFrame(np.random.randn(5, 6), index=np.arange(5, dtype=int), columns=columns) 

243 

244 return df 

245 

246 

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

248 """Make an astropy table for testing. 

249 

250 Parameters 

251 ---------- 

252 include_multidim : `bool` 

253 Include multi-dimensional columns. 

254 include_masked : `bool` 

255 Include masked columns. 

256 include_bigendian : `bool` 

257 Include big-endian columns. 

258 

259 Returns 

260 ------- 

261 astropyTable : `astropy.table.Table` 

262 The test table. 

263 """ 

264 data = _makeSimpleNumpyTable(include_multidim=include_multidim, include_bigendian=include_bigendian) 

265 # Add a couple of units. 

266 table = atable.Table(data) 

267 table["a"].unit = units.degree 

268 table["a"].description = "Description of column a" 

269 table["b"].unit = units.meter 

270 table["b"].description = "Description of column b" 

271 

272 # Add some masked columns. 

273 if include_masked: 

274 nrow = len(table) 

275 mask = np.zeros(nrow, dtype=bool) 

276 mask[1] = True 

277 # We set the masked columns with the underlying sentinel value 

278 # to be able test after serialization. 

279 

280 # Masked 64-bit integer. 

281 arr = np.arange(nrow, dtype="i8") 

282 arr[mask] = -1 

283 arr[0] = 1649900760361600113 

284 table["m_i8"] = np.ma.masked_array(data=arr, mask=mask, fill_value=-1) 

285 # Masked 32-bit float. 

286 arr = np.arange(nrow, dtype="f4") 

287 arr[mask] = np.nan 

288 table["m_f4"] = np.ma.masked_array(data=arr, mask=mask, fill_value=np.nan) 

289 # Unmasked 32-bit float with NaNs. 

290 table["um_f4"] = arr 

291 # Masked 64-bit float. 

292 arr = np.arange(nrow, dtype="f8") 

293 arr[mask] = np.nan 

294 table["m_f8"] = np.ma.masked_array(data=arr, mask=mask, fill_value=np.nan) 

295 # Unmasked 64-bit float with NaNs. 

296 table["um_f8"] = arr 

297 # Masked boolean. 

298 arr = np.zeros(nrow, dtype=np.bool_) 

299 arr[mask] = True 

300 table["m_bool"] = np.ma.masked_array(data=arr, mask=mask, fill_value=True) 

301 # Masked unsigned 32-bit unsigned int. 

302 arr = np.arange(nrow, dtype="u4") 

303 arr[mask] = 0 

304 table["m_u4"] = np.ma.masked_array(data=arr, mask=mask, fill_value=0) 

305 # Masked string. 

306 table["m_str"] = np.ma.masked_array(data=np.array(["text"] * nrow), mask=mask, fill_value="") 

307 # Masked bytes. 

308 table["m_byte"] = np.ma.masked_array(data=np.array([b"bytes"] * nrow), mask=mask, fill_value=b"") 

309 

310 return table 

311 

312 

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

314 """Make an arrow table for testing. 

315 

316 Parameters 

317 ---------- 

318 include_multidim : `bool` 

319 Include multi-dimensional columns. 

320 include_masked : `bool` 

321 Include masked columns. 

322 

323 Returns 

324 ------- 

325 arrowTable : `pyarrow.Table` 

326 The test table. 

327 """ 

328 data = _makeSimpleAstropyTable(include_multidim=include_multidim, include_masked=include_masked) 

329 return astropy_to_arrow(data) 

330 

331 

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

333@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterDataFrame without pyarrow.") 

334class ParquetFormatterDataFrameTestCase(unittest.TestCase): 

335 """Tests for ParquetFormatter, DataFrame, using local file datastore.""" 

336 

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

338 

339 def setUp(self): 

340 """Create a new butler root for each test.""" 

341 self.root = makeTestTempDir(TESTDIR) 

342 config = Config(self.configFile) 

343 self.run = "test_run" 

344 self.butler = Butler.from_config( 

345 make_repo_for_test(self.root, config=config), writeable=True, run=self.run 

346 ) 

347 self.enterContext(self.butler) 

348 # No dimensions in dataset type so we don't have to worry about 

349 # inserting dimension data or defining data IDs. 

350 self.datasetType = DatasetType( 

351 "data", dimensions=(), storageClass="DataFrame", universe=self.butler.dimensions 

352 ) 

353 self.butler.registry.registerDatasetType(self.datasetType) 

354 

355 def tearDown(self): 

356 removeTestTempDir(self.root) 

357 

358 def testSingleIndexDataFrame(self): 

359 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True) 

360 

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

362 # Read the whole DataFrame. 

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

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

365 # Read just the column descriptions. 

366 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={}) 

367 self.assertTrue(allColumns.equals(columns2)) 

368 # Read the rowcount. 

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

370 self.assertEqual(rowcount, len(df1)) 

371 # Read the schema. 

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

373 self.assertEqual(schema, DataFrameSchema(df1)) 

374 # Read just some columns a few different ways. 

375 df3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]}) 

376 self.assertTrue(df1.loc[:, ["a", "c"]].equals(df3)) 

377 df4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"}) 

378 self.assertTrue(df1.loc[:, ["a"]].equals(df4)) 

379 df5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]}) 

380 self.assertTrue(df1.loc[:, ["a"]].equals(df5)) 

381 df6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"}) 

382 self.assertTrue(df1.loc[:, ["ddd"]].equals(df6)) 

383 df7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]}) 

384 self.assertTrue(df1.loc[:, ["a"]].equals(df7)) 

385 df8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d*"]}) 

386 self.assertTrue(df1.loc[:, ["ddd", "dtn", "dtu"]].equals(df8)) 

387 df9 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d*", "d*"]}) 

388 self.assertTrue(df1.loc[:, ["ddd", "dtn", "dtu"]].equals(df9)) 

389 # Passing an unrecognized column should be a ValueError. 

390 with self.assertRaises(ValueError): 

391 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]}) 

392 

393 def testSingleIndexDataFrameWithLists(self): 

394 df1, allColumns = _makeSingleIndexDataFrame(include_lists=True) 

395 

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

397 # Read the whole DataFrame. 

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

399 

400 # We need to check the list columns specially because they go 

401 # from lists to arrays. 

402 for col in ["l1", "l2", "l3"]: 

403 for i in range(len(df1)): 

404 self.assertTrue(np.all(df2[col].values[i] == df1[col].values[i])) 

405 

406 def testMultiIndexDataFrame(self): 

407 df1 = _makeMultiIndexDataFrame() 

408 

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

410 # Read the whole DataFrame. 

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

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

413 # Read just the column descriptions. 

414 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={}) 

415 self.assertTrue(df1.columns.equals(columns2)) 

416 self.assertEqual(columns2.names, df1.columns.names) 

417 # Read the rowcount. 

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

419 self.assertEqual(rowcount, len(df1)) 

420 # Read the schema. 

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

422 self.assertEqual(schema, DataFrameSchema(df1)) 

423 # Read just some columns a few different ways. 

424 df3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": {"filter": "g"}}) 

425 self.assertTrue(df1.loc[:, ["g"]].equals(df3)) 

426 df4 = self.butler.get( 

427 self.datasetType, dataId={}, parameters={"columns": {"filter": ["r"], "column": "a"}} 

428 ) 

429 self.assertTrue(df1.loc[:, [("r", "a")]].equals(df4)) 

430 column_list = [("g", "a"), ("r", "c")] 

431 df5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": column_list}) 

432 self.assertTrue(df1.loc[:, column_list].equals(df5)) 

433 column_dict = {"filter": "r", "column": ["a", "b"]} 

434 df6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": column_dict}) 

435 self.assertTrue(df1.loc[:, [("r", "a"), ("r", "b")]].equals(df6)) 

436 # Passing an unrecognized column should be a ValueError. 

437 with self.assertRaises(ValueError): 

438 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d"]}) 

439 

440 def testSingleIndexDataFrameEmptyString(self): 

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

442 df1, _ = _makeSingleIndexDataFrame() 

443 

444 # Set one of the strings to None 

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

446 

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

448 # Read the whole DataFrame. 

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

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

451 

452 def testSingleIndexDataFrameAllEmptyStrings(self): 

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

454 column. 

455 """ 

456 df1, _ = _makeSingleIndexDataFrame() 

457 

458 # Set all of the strings to None 

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

460 

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

462 # Read the whole DataFrame. 

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

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

465 

466 def testLegacyDataFrame(self): 

467 """Test writing a dataframe to parquet via pandas (without additional 

468 metadata) and ensure that we can read it back with all the new 

469 functionality. 

470 """ 

471 df1, allColumns = _makeSingleIndexDataFrame() 

472 

473 if isinstance(df1.index, pd.RangeIndex): 473 ↛ 479line 473 didn't jump to line 479 because the condition on line 473 was never true

474 # Turn the RangeIndex into a regular index or it won't 

475 # give us all the column names. This is necessary for pandas v3. 

476 # Unfortunately, parquet files serialized directly with 

477 # pandas v3 will not report their index column names if 

478 # they are sequential integers. 

479 df1.index = pd.Index(df1.index.to_numpy(), name=df1.index.name) 

480 

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

482 df1.to_parquet(fname) 

483 

484 legacy_type = DatasetType( 

485 "legacy_dataframe", 

486 dimensions=(), 

487 storageClass="DataFrame", 

488 universe=self.butler.dimensions, 

489 ) 

490 self.butler.registry.registerDatasetType(legacy_type) 

491 

492 data_id = {} 

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

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

495 

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

497 

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

499 

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

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

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

503 

504 df3a = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a"]}) 

505 df3b = self.butler.get("legacy_dataframe", dataId={}, parameters={"columns": ["a"]}) 

506 self.assertTrue(df3a.equals(df3b)) 

507 

508 columns2a = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={}) 

509 columns2b = self.butler.get("legacy_dataframe.columns", dataId={}) 

510 self.assertTrue(columns2a.equals(columns2b)) 

511 

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

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

514 self.assertEqual(rowcount2a, rowcount2b) 

515 

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

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

518 self.assertEqual(schema2a, schema2b) 

519 

520 def testDataFrameSchema(self): 

521 tab1 = _makeSimpleArrowTable() 

522 

523 schema = DataFrameSchema.from_arrow(tab1.schema) 

524 

525 self.assertIsInstance(schema.schema, pd.DataFrame) 

526 self.assertEqual(repr(schema), repr(schema._schema)) 

527 self.assertNotEqual(schema, "not_a_schema") 

528 self.assertEqual(schema, schema) 

529 

530 tab2 = _makeMultiIndexDataFrame() 

531 schema2 = DataFrameSchema(tab2) 

532 

533 self.assertNotEqual(schema, schema2) 

534 

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

536 def testWriteSingleIndexDataFrameReadAsAstropyTable(self): 

537 df1, allColumns = _makeSingleIndexDataFrame() 

538 

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

540 

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

542 

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

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

545 

546 # Check reading the columns. 

547 columns = list(tab2.columns.keys()) 

548 columns2 = self.butler.get( 

549 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList" 

550 ) 

551 # We check the set because pandas reorders the columns. 

552 self.assertEqual(set(columns2), set(columns)) 

553 

554 # Check reading the schema. 

555 schema = ArrowAstropySchema(tab2) 

556 schema2 = self.butler.get( 

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

558 ) 

559 

560 # The string types are objectified by pandas, and the order 

561 # will be changed because of pandas indexing. 

562 self.assertEqual(len(schema2.schema.columns), len(schema.schema.columns)) 

563 for name in schema.schema.columns: 

564 self.assertIn(name, schema2.schema.columns) 

565 if schema2.schema[name].dtype != np.dtype("O"): 

566 self.assertEqual(schema2.schema[name].dtype, schema.schema[name].dtype) 

567 

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

569 def testWriteSingleIndexDataFrameWithMaskedColsReadAsAstropyTable(self): 

570 # We need to special-case the write-as-pandas read-as-astropy code 

571 # with masks because pandas has multiple ways to use masked columns. 

572 # (The string column mask handling in particular is frustratingly 

573 # inconsistent.) 

574 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True) 

575 

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

577 

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

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

580 

581 self.assertTrue(df1.columns.equals(tab2_df.columns)) 

582 for name in tab2_df.columns: 

583 col1 = df1[name] 

584 col2 = tab2_df[name] 

585 

586 if col1.hasnans: 

587 notNull = col1.notnull() 

588 self.assertTrue(notNull.equals(col2.notnull())) 

589 # Need to check value-by-value because column may 

590 # be made of objects, depending on what pandas decides. 

591 for index in notNull.values.nonzero()[0]: 

592 self.assertEqual(col1[index], col2[index]) 

593 else: 

594 self.assertTrue(col1.equals(col2)) 

595 

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

597 def testWriteMultiIndexDataFrameReadAsAstropyTable(self): 

598 df1 = _makeMultiIndexDataFrame() 

599 

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

601 

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

603 

604 # This is an odd duck, it doesn't really round-trip. 

605 # This test simply checks that it's readable, but definitely not 

606 # recommended. 

607 

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

609 def testWriteAstropyTableWithMaskedColsReadAsSingleIndexDataFrame(self): 

610 tab1 = _makeSimpleAstropyTable(include_masked=True) 

611 

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

613 

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

615 

616 tab1_df = astropy_to_pandas(tab1) 

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

618 

619 tab2_astropy = pandas_to_astropy(tab2) 

620 for col in tab1.dtype.names: 

621 np.testing.assert_array_equal(tab2_astropy[col], tab1[col]) 

622 if isinstance(tab1[col], atable.column.MaskedColumn): 

623 np.testing.assert_array_equal(tab2_astropy[col].mask, tab1[col].mask) 

624 

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

626 def testWriteSingleIndexDataFrameReadAsArrowTable(self): 

627 df1, allColumns = _makeSingleIndexDataFrame() 

628 

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

630 

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

632 

633 tab2_df = arrow_to_pandas(tab2) 

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

635 

636 # Check reading the columns. 

637 columns = list(tab2.schema.names) 

638 columns2 = self.butler.get( 

639 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList" 

640 ) 

641 # We check the set because pandas reorders the columns. 

642 self.assertEqual(set(columns), set(columns2)) 

643 

644 # Override the component using a dataset type. 

645 columnsType = self.datasetType.makeComponentDatasetType("columns").overrideStorageClass( 

646 "ArrowColumnList" 

647 ) 

648 self.assertEqual(columns2, self.butler.get(columnsType)) 

649 

650 # Check getting a component while overriding the storage class via 

651 # the dataset type. This overrides the parent storage class and then 

652 # selects the component. 

653 columnsType = self.datasetType.overrideStorageClass("ArrowAstropy").makeComponentDatasetType( 

654 "columns" 

655 ) 

656 self.assertEqual(columns2, self.butler.get(columnsType)) 

657 

658 # Check reading the schema. 

659 schema = tab2.schema 

660 schema2 = self.butler.get( 

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

662 ) 

663 

664 # These will not have the same metadata, nor will the string column 

665 # information be maintained. 

666 self.assertEqual(len(schema.names), len(schema2.names)) 

667 for name in schema.names: 

668 if schema.field(name).type not in (pa.string(), pa.binary()): 

669 self.assertEqual(schema.field(name).type, schema2.field(name).type) 

670 

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

672 def testWriteMultiIndexDataFrameReadAsArrowTable(self): 

673 df1 = _makeMultiIndexDataFrame() 

674 

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

676 

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

678 

679 tab2_df = arrow_to_pandas(tab2) 

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

681 

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

683 def testWriteSingleIndexDataFrameReadAsNumpyTable(self): 

684 df1, allColumns = _makeSingleIndexDataFrame() 

685 

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

687 

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

689 

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

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

692 

693 # Check reading the columns. 

694 columns = list(tab2.dtype.names) 

695 columns2 = self.butler.get( 

696 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList" 

697 ) 

698 # We check the set because pandas reorders the columns. 

699 self.assertEqual(set(columns2), set(columns)) 

700 

701 # Check reading the schema. 

702 schema = ArrowNumpySchema(tab2.dtype) 

703 schema2 = self.butler.get( 

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

705 ) 

706 

707 # The string types will be objectified by pandas, and the order 

708 # will be changed because of pandas indexing. 

709 self.assertEqual(len(schema.schema.names), len(schema2.schema.names)) 

710 for name in schema.schema.names: 

711 self.assertIn(name, schema2.schema.names) 

712 # It is not possible to properly track string columns via 

713 # the schema consistently. 

714 if schema.schema[name].type == np.dtype("O") or schema2.schema[name].type == np.dtype("O"): 

715 continue 

716 else: 

717 self.assertEqual(schema2.schema[name].type, schema.schema[name].type) 

718 

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

720 def testWriteMultiIndexDataFrameReadAsNumpyTable(self): 

721 df1 = _makeMultiIndexDataFrame() 

722 

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

724 

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

726 

727 # This is an odd duck, it doesn't really round-trip. 

728 # This test simply checks that it's readable, but definitely not 

729 # recommended. 

730 

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

732 def testWriteSingleIndexDataFrameReadAsNumpyDict(self): 

733 df1, allColumns = _makeSingleIndexDataFrame() 

734 

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

736 

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

738 

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

740 # The column order is not maintained. 

741 self.assertEqual(set(df1.columns), set(tab2_df.columns)) 

742 for col in df1.columns: 

743 self.assertTrue(np.all(df1[col].values == tab2_df[col].values)) 

744 

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

746 def testWriteMultiIndexDataFrameReadAsNumpyDict(self): 

747 df1 = _makeMultiIndexDataFrame() 

748 

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

750 

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

752 

753 # This is an odd duck, it doesn't really round-trip. 

754 # This test simply checks that it's readable, but definitely not 

755 # recommended. 

756 

757 def testBadDataFrameColumnParquet(self): 

758 df1, allColumns = _makeSingleIndexDataFrame() 

759 

760 # Make a column with mixed type. 

761 bad_col1 = [0.0] * len(df1) 

762 bad_col1[1] = 0.0 * units.nJy 

763 bad_df = df1.copy() 

764 bad_df["bad_col1"] = bad_col1 

765 

766 # At the moment we cannot check that the correct note is added 

767 # to the exception, but that will be possible in the future. 

768 with self.assertRaises(RuntimeError): 

769 self.butler.put(bad_df, self.datasetType, dataId={}) 

770 

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

772 def testWriteReadAstropyTableLossless(self): 

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

774 

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

776 

777 tab2 = self.butler.get( 

778 self.datasetType, 

779 dataId={}, 

780 storageClass="ArrowAstropy", 

781 parameters={"strip_astropy_meta_yaml": False}, 

782 ) 

783 

784 # Check that minimal provenance was written by default. 

785 expected = { 

786 "LSST.BUTLER.ID": str(put_ref.id), 

787 "LSST.BUTLER.RUN": "test_run", 

788 "LSST.BUTLER.DATASETTYPE": "data", 

789 "LSST.BUTLER.N_INPUTS": 0, 

790 } 

791 

792 self.assertEqual(tab2.meta, expected) 

793 

794 _checkAstropyTableEquality(tab1, tab2) 

795 

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

797 def testWriteReadAstropyTableProvenance(self): 

798 tab1 = _makeSimpleAstropyTable() 

799 

800 # Create a ref for provenance. 

801 astropy_type = DatasetType( 

802 "astropy_parquet", 

803 dimensions=(), 

804 storageClass="ArrowAstropy", 

805 universe=self.butler.dimensions, 

806 ) 

807 self.butler.registry.registerDatasetType(astropy_type) 

808 input_ref = DatasetRef(astropy_type, {}, run="other_run") 

809 quantum_id = uuid.uuid4() 

810 provenance = DatasetProvenance(quantum_id=quantum_id) 

811 provenance.add_input(input_ref) 

812 

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

814 

815 tab2 = self.butler.get( 

816 self.datasetType, 

817 dataId={}, 

818 storageClass="ArrowAstropy", 

819 parameters={"strip_astropy_meta_yaml": False}, 

820 ) 

821 

822 expected = { 

823 "LSST.BUTLER.ID": str(put_ref.id), 

824 "LSST.BUTLER.RUN": "test_run", 

825 "LSST.BUTLER.DATASETTYPE": "data", 

826 "LSST.BUTLER.QUANTUM": str(quantum_id), 

827 "LSST.BUTLER.N_INPUTS": 1, 

828 "LSST.BUTLER.INPUT.0.ID": str(input_ref.id), 

829 "LSST.BUTLER.INPUT.0.RUN": "other_run", 

830 "LSST.BUTLER.INPUT.0.DATASETTYPE": "astropy_parquet", 

831 } 

832 self.assertEqual(tab2.meta, expected) 

833 

834 # Put the dataset again, with different provenance and ensure 

835 # that the previous provenance was stripped. 

836 self.butler.collections.register("new_run") 

837 put_ref3 = self.butler.put(tab2, self.datasetType, dataId={}, run="new_run") 

838 

839 # tab2 will have been updated in place. 

840 expected = { 

841 "LSST.BUTLER.ID": str(put_ref3.id), 

842 "LSST.BUTLER.RUN": "new_run", 

843 "LSST.BUTLER.DATASETTYPE": "data", 

844 "LSST.BUTLER.N_INPUTS": 0, 

845 } 

846 self.assertEqual(tab2.meta, expected) 

847 null_prov, prov_ref = DatasetProvenance.from_flat_dict(tab2.meta, self.butler) 

848 self.assertEqual(prov_ref, put_ref3) 

849 self.assertEqual(null_prov, DatasetProvenance()) 

850 

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

852 def testWriteReadNumpyTableLossless(self): 

853 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

854 

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

856 

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

858 

859 _checkNumpyTableEquality(tab1, tab2) 

860 

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

862 def testMaskedNumpy(self): 

863 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=True) 

864 tab1_np = arrow_to_numpy(tab1) 

865 self.assertIsInstance(tab1_np, np.ma.MaskedArray) 

866 # Stats on a masked column should ignore the nan in row 1. 

867 col = tab1_np["m_f8"] 

868 self.assertEqual(np.mean(col), 2.25, f"Column: {col}") 

869 

870 # Now without a mask. 

871 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=False) 

872 tab1_np = arrow_to_numpy(tab1) 

873 self.assertNotIsInstance(tab1_np, np.ma.MaskedArray) 

874 

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

876 def testWriteReadArrowTableLossless(self): 

877 tab1 = _makeSimpleArrowTable(include_multidim=False, include_masked=True) 

878 

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

880 

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

882 

883 self.assertEqual(tab1.schema, tab2.schema) 

884 tab1_np = arrow_to_numpy(tab1) 

885 tab2_np = arrow_to_numpy(tab2) 

886 for col in tab1.column_names: 

887 np.testing.assert_array_equal(tab2_np[col], tab1_np[col]) 

888 

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

890 def testWriteReadNumpyDictLossless(self): 

891 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

892 dict1 = _numpy_to_numpy_dict(tab1) 

893 

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

895 

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

897 

898 _checkNumpyDictEquality(dict1, dict2) 

899 

900 

901@unittest.skipUnless(pd is not None, "Cannot test InMemoryDatastore with DataFrames without pandas.") 

902class InMemoryDataFrameDelegateTestCase(ParquetFormatterDataFrameTestCase): 

903 """Tests for InMemoryDatastore, using ArrowTableDelegate with Dataframe.""" 

904 

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

906 

907 def testBadDataFrameColumnParquet(self): 

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

909 pass 

910 

911 def testWriteMultiIndexDataFrameReadAsAstropyTable(self): 

912 df1 = _makeMultiIndexDataFrame() 

913 

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

915 

916 with self.assertRaises(ValueError): 

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

918 

919 def testLegacyDataFrame(self): 

920 # This test does not work with an inMemoryDatastore. 

921 pass 

922 

923 def testBadInput(self): 

924 df1, _ = _makeSingleIndexDataFrame() 

925 delegate = ArrowTableDelegate("DataFrame") 

926 

927 with self.assertRaises(ValueError): 

928 delegate.handleParameters(inMemoryDataset="not_a_dataframe") 

929 

930 with self.assertRaises(AttributeError): 

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

932 

933 def testStorageClass(self): 

934 df1, allColumns = _makeSingleIndexDataFrame() 

935 

936 factory = StorageClassFactory() 

937 factory.addFromConfig(StorageClassConfig()) 

938 

939 storageClass = factory.findStorageClass(type(df1), compare_types=False) 

940 # Force the name lookup to do name matching. 

941 storageClass._pytype = None 

942 self.assertEqual(storageClass.name, "DataFrame") 

943 

944 storageClass = factory.findStorageClass(type(df1), compare_types=True) 

945 # Force the name lookup to do name matching. 

946 storageClass._pytype = None 

947 self.assertEqual(storageClass.name, "DataFrame") 

948 

949 

950@unittest.skipUnless(atable is not None, "Cannot test ParquetFormatterArrowAstropy without astropy.") 

951@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowAstropy without pyarrow.") 

952class ParquetFormatterArrowAstropyTestCase(unittest.TestCase): 

953 """Tests for ParquetFormatter, ArrowAstropy, using local file datastore.""" 

954 

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

956 

957 def setUp(self): 

958 """Create a new butler root for each test.""" 

959 self.root = makeTestTempDir(TESTDIR) 

960 config = Config(self.configFile) 

961 self.run = "test_run" 

962 self.butler = Butler.from_config( 

963 make_repo_for_test(self.root, config=config), writeable=True, run=self.run 

964 ) 

965 self.enterContext(self.butler) 

966 # No dimensions in dataset type so we don't have to worry about 

967 # inserting dimension data or defining data IDs. 

968 self.datasetType = DatasetType( 

969 "data", dimensions=(), storageClass="ArrowAstropy", universe=self.butler.dimensions 

970 ) 

971 self.butler.registry.registerDatasetType(self.datasetType) 

972 

973 def tearDown(self): 

974 removeTestTempDir(self.root) 

975 

976 def testAstropyTable(self): 

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

978 

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

980 # Read the whole Table. 

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

982 _checkAstropyTableEquality(tab1, tab2) 

983 # Read the columns. 

984 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={}) 

985 self.assertEqual(len(columns2), len(tab1.dtype.names)) 

986 for i, name in enumerate(tab1.dtype.names): 

987 self.assertEqual(columns2[i], name) 

988 # Read the rowcount. 

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

990 self.assertEqual(rowcount, len(tab1)) 

991 # Read the schema. 

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

993 self.assertEqual(schema, ArrowAstropySchema(tab1)) 

994 # Read just some columns a few different ways. 

995 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]}) 

996 _checkAstropyTableEquality(tab1[("a", "c")], tab3) 

997 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"}) 

998 _checkAstropyTableEquality(tab1[("a",)], tab4) 

999 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]}) 

1000 _checkAstropyTableEquality(tab1[("index", "a")], tab5) 

1001 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"}) 

1002 _checkAstropyTableEquality(tab1[("ddd",)], tab6) 

1003 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]}) 

1004 _checkAstropyTableEquality(tab1[("a",)], tab7) 

1005 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??"]}) 

1006 _checkAstropyTableEquality(tab1[("ddd", "dtn", "dtu")], tab8) 

1007 tab9 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]}) 

1008 _checkAstropyTableEquality(tab1[("ddd", "dtn", "dtu", "a")], tab9) 

1009 # Passing an unrecognized column should be a ValueError. 

1010 with self.assertRaises(ValueError): 

1011 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]}) 

1012 

1013 def testAstropyTableBigEndian(self): 

1014 tab1 = _makeSimpleAstropyTable(include_bigendian=True) 

1015 

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

1017 # Read the whole Table. 

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

1019 _checkAstropyTableEquality(tab1, tab2, has_bigendian=True) 

1020 

1021 def testAstropyTableWithMetadata(self): 

1022 tab1 = _makeSimpleAstropyTable(include_multidim=True) 

1023 

1024 meta = { 

1025 "meta_a": 5, 

1026 "meta_b": 10.0, 

1027 "meta_c": [1, 2, 3], 

1028 "meta_d": True, 

1029 "meta_e": "string", 

1030 } 

1031 

1032 tab1.meta.update(meta) 

1033 

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

1035 # Read the whole Table. 

1036 tab2 = self.butler.get(self.datasetType, dataId={}, parameters={"strip_astropy_meta_yaml": False}) 

1037 # This will check that the metadata is equivalent as well. 

1038 _checkAstropyTableEquality(tab1, tab2) 

1039 

1040 def testArrowAstropySchema(self): 

1041 tab1 = _makeSimpleAstropyTable() 

1042 tab1_arrow = astropy_to_arrow(tab1) 

1043 schema = ArrowAstropySchema.from_arrow(tab1_arrow.schema) 

1044 

1045 self.assertIsInstance(schema.schema, atable.Table) 

1046 self.assertEqual(repr(schema), repr(schema._schema)) 

1047 self.assertNotEqual(schema, "not_a_schema") 

1048 self.assertEqual(schema, schema) 

1049 

1050 # Test various inequalities 

1051 tab2 = tab1.copy() 

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

1053 schema2 = ArrowAstropySchema(tab2) 

1054 self.assertNotEqual(schema2, schema) 

1055 

1056 tab2 = tab1.copy() 

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

1058 schema2 = ArrowAstropySchema(tab2) 

1059 self.assertNotEqual(schema2, schema) 

1060 

1061 tab2 = tab1.copy() 

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

1063 schema2 = ArrowAstropySchema(tab2) 

1064 self.assertNotEqual(schema2, schema) 

1065 

1066 tab2 = tab1.copy() 

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

1068 schema2 = ArrowAstropySchema(tab2) 

1069 self.assertNotEqual(schema2, schema) 

1070 

1071 def testAstropyParquet(self): 

1072 tab1 = _makeSimpleAstropyTable() 

1073 

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

1075 del tab1["dtn"] 

1076 del tab1["dtu"] 

1077 

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

1079 tab1.write(fname) 

1080 

1081 astropy_type = DatasetType( 

1082 "astropy_parquet", 

1083 dimensions=(), 

1084 storageClass="ArrowAstropy", 

1085 universe=self.butler.dimensions, 

1086 ) 

1087 self.butler.registry.registerDatasetType(astropy_type) 

1088 

1089 data_id = {} 

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

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

1092 

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

1094 

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

1096 

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

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

1099 _checkAstropyTableEquality(tab2a, tab2b) 

1100 

1101 columns2a = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={}) 

1102 columns2b = self.butler.get("astropy_parquet.columns", dataId={}) 

1103 self.assertEqual(len(columns2b), len(columns2a)) 

1104 for i, name in enumerate(columns2a): 

1105 self.assertEqual(columns2b[i], name) 

1106 

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

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

1109 self.assertEqual(rowcount2a, rowcount2b) 

1110 

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

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

1113 self.assertEqual(schema2a, schema2b) 

1114 

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

1116 def testWriteAstropyReadAsArrowTable(self): 

1117 # This astropy <-> arrow works fine with masked columns. 

1118 tab1 = _makeSimpleAstropyTable(include_masked=True) 

1119 

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

1121 

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

1123 

1124 tab2_astropy = arrow_to_astropy(tab2) 

1125 _checkAstropyTableEquality(tab1, tab2_astropy) 

1126 

1127 # Check reading the columns. 

1128 columns = tab2.schema.names 

1129 columns2 = self.butler.get( 

1130 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList" 

1131 ) 

1132 self.assertEqual(columns2, columns) 

1133 

1134 # Check reading the schema. 

1135 schema = tab2.schema 

1136 schema2 = self.butler.get( 

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

1138 ) 

1139 

1140 self.assertEqual(schema, schema2) 

1141 

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

1143 def testWriteAstropyReadAsDataFrame(self): 

1144 tab1 = _makeSimpleAstropyTable() 

1145 

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

1147 

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

1149 

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

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

1152 

1153 tab1_df = tab1.to_pandas() 

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

1155 

1156 # Check reading the columns. 

1157 columns = tab2.columns 

1158 columns2 = self.butler.get( 

1159 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex" 

1160 ) 

1161 self.assertTrue(columns.equals(columns2)) 

1162 

1163 # Check reading the schema. 

1164 schema = DataFrameSchema(tab2) 

1165 schema2 = self.butler.get( 

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

1167 ) 

1168 

1169 self.assertEqual(schema2, schema) 

1170 

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

1172 def testWriteAstropyWithMaskedColsReadAsDataFrame(self): 

1173 # We need to special-case the write-as-astropy read-as-pandas code 

1174 # with masks because pandas has multiple ways to use masked columns. 

1175 # (When writing an astropy table with masked columns we get an object 

1176 # column back, but each unmasked element has the correct type.) 

1177 tab1 = _makeSimpleAstropyTable(include_masked=True) 

1178 

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

1180 

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

1182 

1183 tab1_df = astropy_to_pandas(tab1) 

1184 

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

1186 for name in tab2.columns: 

1187 col1 = tab1_df[name] 

1188 col2 = tab2[name] 

1189 

1190 if col1.hasnans: 

1191 notNull = col1.notnull() 

1192 self.assertTrue(notNull.equals(col2.notnull())) 

1193 # Need to check value-by-value because column may 

1194 # be made of objects, depending on what pandas decides. 

1195 for index in notNull.values.nonzero()[0]: 

1196 self.assertEqual(col1[index], col2[index]) 

1197 else: 

1198 self.assertTrue(col1.equals(col2)) 

1199 

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

1201 def testWriteSingleIndexDataFrameWithMaskedColsReadAsAstropyTable(self): 

1202 df1, allColumns = _makeSingleIndexDataFrame(include_masked=True) 

1203 

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

1205 

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

1207 

1208 df1_tab = pandas_to_astropy(df1) 

1209 

1210 _checkAstropyTableEquality(df1_tab, tab2) 

1211 

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

1213 def testWriteAstropyReadAsNumpyTable(self): 

1214 tab1 = _makeSimpleAstropyTable() 

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

1216 

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

1218 

1219 # This is tricky because it loses the units. 

1220 tab2_astropy = atable.Table(tab2) 

1221 

1222 _checkAstropyTableEquality(tab1, tab2_astropy, skip_units=True) 

1223 

1224 # Check reading the columns. 

1225 columns = list(tab2.dtype.names) 

1226 columns2 = self.butler.get( 

1227 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList" 

1228 ) 

1229 self.assertEqual(columns2, columns) 

1230 

1231 # Check reading the schema. 

1232 schema = ArrowNumpySchema(tab2.dtype) 

1233 schema2 = self.butler.get( 

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

1235 ) 

1236 

1237 self.assertEqual(schema2, schema) 

1238 

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

1240 def testWriteAstropyReadAsNumpyDict(self): 

1241 tab1 = _makeSimpleAstropyTable() 

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

1243 

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

1245 

1246 # This is tricky because it loses the units. 

1247 tab2_astropy = atable.Table(tab2) 

1248 

1249 _checkAstropyTableEquality(tab1, tab2_astropy, skip_units=True) 

1250 

1251 def testBadAstropyColumnParquet(self): 

1252 tab1 = _makeSimpleAstropyTable() 

1253 

1254 # Make a column with mixed type. 

1255 bad_col1 = [0.0] * len(tab1) 

1256 bad_col1[1] = 0.0 * units.nJy 

1257 bad_tab = tab1.copy() 

1258 bad_tab["bad_col1"] = bad_col1 

1259 

1260 # At the moment we cannot check that the correct note is added 

1261 # to the exception, but that will be possible in the future. 

1262 with self.assertRaises(RuntimeError): 

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

1264 

1265 # Make a column with ragged size. 

1266 bad_col2 = [[0]] * len(tab1) 

1267 bad_col2[1] = [0, 0] 

1268 bad_tab = tab1.copy() 

1269 bad_tab["bad_col2"] = bad_col2 

1270 

1271 with self.assertRaises(RuntimeError): 

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

1273 

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

1275 def testWriteAstropyTableWithPandasIndexHint(self, testStrip=True): 

1276 tab1 = _makeSimpleAstropyTable() 

1277 

1278 add_pandas_index_to_astropy(tab1, "index") 

1279 

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

1281 

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

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

1284 

1285 self.assertIn(ASTROPY_PANDAS_INDEX_KEY, tab2.meta) 

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

1287 

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

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

1290 

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

1292 

1293 # Read as a dataframe without naming the index column. 

1294 with self.assertLogs(level="WARNING") as cm: 

1295 _ = self.butler.get( 

1296 self.datasetType, 

1297 dataId={}, 

1298 storageClass="DataFrame", 

1299 parameters={"columns": ["a", "b"]}, 

1300 ) 

1301 self.assertIn("Index column ``index``", cm.output[0]) 

1302 

1303 if testStrip: 

1304 # Read as an astropy table without naming the index column. 

1305 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "b"]}) 

1306 

1307 self.assertNotIn(ASTROPY_PANDAS_INDEX_KEY, tab5.meta) 

1308 

1309 with self.assertRaises(ValueError): 

1310 add_pandas_index_to_astropy(tab1, "not_a_column") 

1311 

1312 

1313@unittest.skipUnless(atable is not None, "Cannot test InMemoryDatastore with AstropyTable without astropy.") 

1314class InMemoryArrowAstropyDelegateTestCase(ParquetFormatterArrowAstropyTestCase): 

1315 """Tests for InMemoryDatastore, using ArrowTableDelegate with 

1316 AstropyTable. 

1317 """ 

1318 

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

1320 

1321 def testAstropyParquet(self): 

1322 # This test does not work with an inMemoryDatastore. 

1323 pass 

1324 

1325 def testBadAstropyColumnParquet(self): 

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

1327 pass 

1328 

1329 def testBadInput(self): 

1330 tab1 = _makeSimpleAstropyTable() 

1331 delegate = ArrowTableDelegate("ArrowAstropy") 

1332 

1333 with self.assertRaises(ValueError): 

1334 delegate.handleParameters(inMemoryDataset="not_an_astropy_table") 

1335 

1336 with self.assertRaises(NotImplementedError): 

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

1338 

1339 with self.assertRaises(AttributeError): 

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

1341 

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

1343 def testWriteAstropyTableWithPandasIndexHint(self): 

1344 super().testWriteAstropyTableWithPandasIndexHint(testStrip=False) 

1345 

1346 

1347@unittest.skipUnless(np is not None, "Cannot test ParquetFormatterArrowNumpy without numpy.") 

1348@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowNumpy without pyarrow.") 

1349class ParquetFormatterArrowNumpyTestCase(unittest.TestCase): 

1350 """Tests for ParquetFormatter, ArrowNumpy, using local file datastore.""" 

1351 

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

1353 

1354 def setUp(self): 

1355 """Create a new butler root for each test.""" 

1356 self.root = makeTestTempDir(TESTDIR) 

1357 config = Config(self.configFile) 

1358 self.butler = Butler.from_config( 

1359 make_repo_for_test(self.root, config=config), writeable=True, run="test_run" 

1360 ) 

1361 self.enterContext(self.butler) 

1362 # No dimensions in dataset type so we don't have to worry about 

1363 # inserting dimension data or defining data IDs. 

1364 self.datasetType = DatasetType( 

1365 "data", dimensions=(), storageClass="ArrowNumpy", universe=self.butler.dimensions 

1366 ) 

1367 self.butler.registry.registerDatasetType(self.datasetType) 

1368 

1369 def tearDown(self): 

1370 removeTestTempDir(self.root) 

1371 

1372 def testNumpyTable(self): 

1373 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1374 

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

1376 # Read the whole Table. 

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

1378 _checkNumpyTableEquality(tab1, tab2) 

1379 # Read the columns. 

1380 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={}) 

1381 self.assertEqual(len(columns2), len(tab1.dtype.names)) 

1382 for i, name in enumerate(tab1.dtype.names): 

1383 self.assertEqual(columns2[i], name) 

1384 # Read the rowcount. 

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

1386 self.assertEqual(rowcount, len(tab1)) 

1387 # Read the schema. 

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

1389 self.assertEqual(schema, ArrowNumpySchema(tab1.dtype)) 

1390 # Read just some columns a few different ways. 

1391 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]}) 

1392 _checkNumpyTableEquality(tab1[["a", "c"]], tab3) 

1393 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"}) 

1394 _checkNumpyTableEquality( 

1395 tab1[ 

1396 [ 

1397 "a", 

1398 ] 

1399 ], 

1400 tab4, 

1401 ) 

1402 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]}) 

1403 _checkNumpyTableEquality(tab1[["index", "a"]], tab5) 

1404 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"}) 

1405 _checkNumpyTableEquality( 

1406 tab1[ 

1407 [ 

1408 "ddd", 

1409 ] 

1410 ], 

1411 tab6, 

1412 ) 

1413 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]}) 

1414 _checkNumpyTableEquality( 

1415 tab1[ 

1416 [ 

1417 "a", 

1418 ] 

1419 ], 

1420 tab7, 

1421 ) 

1422 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]}) 

1423 _checkNumpyTableEquality( 

1424 tab1[ 

1425 [ 

1426 "ddd", 

1427 "dtn", 

1428 "dtu", 

1429 "a", 

1430 ] 

1431 ], 

1432 tab8, 

1433 ) 

1434 # Passing an unrecognized column should be a ValueError. 

1435 with self.assertRaises(ValueError): 

1436 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]}) 

1437 

1438 def testNumpyTableBigEndian(self): 

1439 tab1 = _makeSimpleNumpyTable(include_bigendian=True) 

1440 

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

1442 # Read the whole Table. 

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

1444 _checkNumpyTableEquality(tab1, tab2, has_bigendian=True) 

1445 

1446 def testArrowNumpySchema(self): 

1447 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1448 tab1_arrow = numpy_to_arrow(tab1) 

1449 schema = ArrowNumpySchema.from_arrow(tab1_arrow.schema) 

1450 

1451 self.assertIsInstance(schema.schema, np.dtype) 

1452 self.assertEqual(repr(schema), repr(schema._dtype)) 

1453 self.assertNotEqual(schema, "not_a_schema") 

1454 self.assertEqual(schema, schema) 

1455 

1456 # Test inequality 

1457 tab2 = tab1.copy() 

1458 names = list(tab2.dtype.names) 

1459 names[0] = "index2" 

1460 tab2.dtype.names = names 

1461 schema2 = ArrowNumpySchema(tab2.dtype) 

1462 self.assertNotEqual(schema2, schema) 

1463 

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

1465 def testNumpyDictConversions(self): 

1466 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1467 

1468 # Verify that everything round-trips, including the schema. 

1469 tab1_arrow = numpy_to_arrow(tab1) 

1470 tab1_dict = arrow_to_numpy_dict(tab1_arrow) 

1471 tab1_dict_arrow = numpy_dict_to_arrow(tab1_dict) 

1472 

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

1474 self.assertEqual(tab1_arrow, tab1_dict_arrow) 

1475 

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

1477 def testWriteNumpyTableReadAsArrowTable(self): 

1478 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1479 

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

1481 

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

1483 

1484 tab2_numpy = arrow_to_numpy(tab2) 

1485 

1486 _checkNumpyTableEquality(tab1, tab2_numpy) 

1487 

1488 # Check reading the columns. 

1489 columns = tab2.schema.names 

1490 columns2 = self.butler.get( 

1491 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList" 

1492 ) 

1493 self.assertEqual(columns2, columns) 

1494 

1495 # Check reading the schema. 

1496 schema = tab2.schema 

1497 schema2 = self.butler.get( 

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

1499 ) 

1500 self.assertEqual(schema2, schema) 

1501 

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

1503 def testWriteNumpyTableReadAsDataFrame(self): 

1504 tab1 = _makeSimpleNumpyTable() 

1505 

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

1507 

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

1509 

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

1511 # and changes the datatype of the string column. 

1512 

1513 tab1_df = pd.DataFrame(tab1) 

1514 

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

1516 

1517 # Check reading the columns. 

1518 columns = tab2.columns 

1519 columns2 = self.butler.get( 

1520 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex" 

1521 ) 

1522 self.assertTrue(columns.equals(columns2)) 

1523 

1524 # Check reading the schema. 

1525 schema = DataFrameSchema(tab2) 

1526 schema2 = self.butler.get( 

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

1528 ) 

1529 

1530 self.assertEqual(schema2, schema) 

1531 

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

1533 def testWriteNumpyTableReadAsAstropyTable(self): 

1534 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1535 

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

1537 

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

1539 tab2_numpy = tab2.as_array() 

1540 

1541 _checkNumpyTableEquality(tab1, tab2_numpy) 

1542 

1543 # Check reading the columns. 

1544 columns = list(tab2.columns.keys()) 

1545 columns2 = self.butler.get( 

1546 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList" 

1547 ) 

1548 self.assertEqual(columns2, columns) 

1549 

1550 # Check reading the schema. 

1551 schema = ArrowAstropySchema(tab2) 

1552 schema2 = self.butler.get( 

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

1554 ) 

1555 

1556 self.assertEqual(schema2, schema) 

1557 

1558 def testWriteNumpyTableReadAsNumpyDict(self): 

1559 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1560 

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

1562 

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

1564 tab2_numpy = _numpy_dict_to_numpy(tab2) 

1565 

1566 _checkNumpyTableEquality(tab1, tab2_numpy) 

1567 

1568 def testBadNumpyColumnParquet(self): 

1569 tab1 = _makeSimpleAstropyTable() 

1570 

1571 # Make a column with mixed type. 

1572 bad_col1 = [0.0] * len(tab1) 

1573 bad_col1[1] = 0.0 * units.nJy 

1574 bad_tab = tab1.copy() 

1575 bad_tab["bad_col1"] = bad_col1 

1576 

1577 bad_tab_np = bad_tab.as_array() 

1578 

1579 # At the moment we cannot check that the correct note is added 

1580 # to the exception, but that will be possible in the future. 

1581 with self.assertRaises(RuntimeError): 

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

1583 

1584 # Make a column with ragged size. 

1585 bad_col2 = [[0]] * len(tab1) 

1586 bad_col2[1] = [0, 0] 

1587 bad_tab = tab1.copy() 

1588 bad_tab["bad_col2"] = bad_col2 

1589 

1590 bad_tab_np = bad_tab.as_array() 

1591 

1592 with self.assertRaises(RuntimeError): 

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

1594 

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

1596 def testWriteReadAstropyTableLossless(self): 

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

1598 

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

1600 

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

1602 

1603 _checkAstropyTableEquality(tab1, tab2) 

1604 

1605 

1606@unittest.skipUnless(np is not None, "Cannot test ImMemoryDatastore with Numpy table without numpy.") 

1607class InMemoryArrowNumpyDelegateTestCase(ParquetFormatterArrowNumpyTestCase): 

1608 """Tests for InMemoryDatastore, using ArrowTableDelegate with 

1609 Numpy table. 

1610 """ 

1611 

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

1613 

1614 def testBadNumpyColumnParquet(self): 

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

1616 pass 

1617 

1618 def testBadInput(self): 

1619 tab1 = _makeSimpleNumpyTable() 

1620 delegate = ArrowTableDelegate("ArrowNumpy") 

1621 

1622 with self.assertRaises(ValueError): 

1623 delegate.handleParameters(inMemoryDataset="not_a_numpy_table") 

1624 

1625 with self.assertRaises(NotImplementedError): 

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

1627 

1628 with self.assertRaises(AttributeError): 

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

1630 

1631 def testStorageClass(self): 

1632 tab1 = _makeSimpleNumpyTable() 

1633 

1634 factory = StorageClassFactory() 

1635 factory.addFromConfig(StorageClassConfig()) 

1636 

1637 storageClass = factory.findStorageClass(type(tab1), compare_types=False) 

1638 # Force the name lookup to do name matching. 

1639 storageClass._pytype = None 

1640 self.assertEqual(storageClass.name, "ArrowNumpy") 

1641 

1642 storageClass = factory.findStorageClass(type(tab1), compare_types=True) 

1643 # Force the name lookup to do name matching. 

1644 storageClass._pytype = None 

1645 self.assertEqual(storageClass.name, "ArrowNumpy") 

1646 

1647 

1648@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowTable without pyarrow.") 

1649class ParquetFormatterArrowTableTestCase(unittest.TestCase): 

1650 """Tests for ParquetFormatter, ArrowTable, using local file datastore.""" 

1651 

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

1653 

1654 def setUp(self): 

1655 """Create a new butler root for each test.""" 

1656 self.root = makeTestTempDir(TESTDIR) 

1657 config = Config(self.configFile) 

1658 self.butler = Butler.from_config( 

1659 make_repo_for_test(self.root, config=config), writeable=True, run="test_run" 

1660 ) 

1661 self.enterContext(self.butler) 

1662 # No dimensions in dataset type so we don't have to worry about 

1663 # inserting dimension data or defining data IDs. 

1664 self.datasetType = DatasetType( 

1665 "data", dimensions=(), storageClass="ArrowTable", universe=self.butler.dimensions 

1666 ) 

1667 self.butler.registry.registerDatasetType(self.datasetType) 

1668 

1669 def tearDown(self): 

1670 removeTestTempDir(self.root) 

1671 

1672 def testArrowTable(self): 

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

1674 

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

1676 # Read the whole Table. 

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

1678 # We convert to use the numpy testing framework to handle nan 

1679 # comparisons. 

1680 self.assertEqual(tab1.schema, tab2.schema) 

1681 tab1_np = arrow_to_numpy(tab1) 

1682 tab2_np = arrow_to_numpy(tab2) 

1683 for col in tab1.column_names: 

1684 np.testing.assert_array_equal(tab2_np[col], tab1_np[col]) 

1685 # Read the columns. 

1686 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={}) 

1687 self.assertEqual(len(columns2), len(tab1.schema.names)) 

1688 for i, name in enumerate(tab1.schema.names): 

1689 self.assertEqual(columns2[i], name) 

1690 # Read the rowcount. 

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

1692 self.assertEqual(rowcount, len(tab1)) 

1693 # Read the schema. 

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

1695 self.assertEqual(schema, tab1.schema) 

1696 # Read just some columns a few different ways. 

1697 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]}) 

1698 self.assertEqual(tab3, tab1.select(("a", "c"))) 

1699 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"}) 

1700 self.assertEqual(tab4, tab1.select(("a",))) 

1701 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]}) 

1702 self.assertEqual(tab5, tab1.select(("index", "a"))) 

1703 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"}) 

1704 self.assertEqual(tab6, tab1.select(("ddd",))) 

1705 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]}) 

1706 self.assertEqual(tab7, tab1.select(("a",))) 

1707 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a*", "d??"]}) 

1708 self.assertEqual(tab8, tab1.select(("a", "ddd", "dtn", "dtu"))) 

1709 # Passing an unrecognized column should be a ValueError. 

1710 with self.assertRaises(ValueError): 

1711 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]}) 

1712 

1713 def testEmptyArrowTable(self): 

1714 data = _makeSimpleNumpyTable() 

1715 type_list = _numpy_dtype_to_arrow_types(data.dtype) 

1716 

1717 schema = pa.schema(type_list) 

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

1719 

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

1721 

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

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

1724 self.assertEqual(tab2, tab1) 

1725 

1726 tab1_numpy = arrow_to_numpy(tab1) 

1727 self.assertEqual(len(tab1_numpy), 0) 

1728 tab1_numpy_arrow = numpy_to_arrow(tab1_numpy) 

1729 self.assertEqual(tab1_numpy_arrow, tab1) 

1730 

1731 tab1_pandas = arrow_to_pandas(tab1) 

1732 self.assertEqual(len(tab1_pandas), 0) 

1733 tab1_pandas_arrow = pandas_to_arrow(tab1_pandas) 

1734 # Unfortunately, string/byte columns get mangled when translated 

1735 # through empty pandas dataframes. 

1736 self.assertEqual( 

1737 tab1_pandas_arrow.select(("index", "a", "b", "c", "ddd")), 

1738 tab1.select(("index", "a", "b", "c", "ddd")), 

1739 ) 

1740 

1741 tab1_astropy = arrow_to_astropy(tab1) 

1742 self.assertEqual(len(tab1_astropy), 0) 

1743 tab1_astropy_arrow = astropy_to_arrow(tab1_astropy) 

1744 self.assertEqual(tab1_astropy_arrow, tab1) 

1745 

1746 def testEmptyArrowTableMultidim(self): 

1747 data = _makeSimpleNumpyTable(include_multidim=True) 

1748 type_list = _numpy_dtype_to_arrow_types(data.dtype) 

1749 

1750 md = {} 

1751 for name in data.dtype.names: 

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

1753 

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

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

1756 

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

1758 

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

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

1761 self.assertEqual(tab2, tab1) 

1762 

1763 tab1_numpy = arrow_to_numpy(tab1) 

1764 self.assertEqual(len(tab1_numpy), 0) 

1765 tab1_numpy_arrow = numpy_to_arrow(tab1_numpy) 

1766 self.assertEqual(tab1_numpy_arrow, tab1) 

1767 

1768 tab1_astropy = arrow_to_astropy(tab1) 

1769 self.assertEqual(len(tab1_astropy), 0) 

1770 tab1_astropy_arrow = astropy_to_arrow(tab1_astropy) 

1771 self.assertEqual(tab1_astropy_arrow, tab1) 

1772 

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

1774 def testWriteArrowTableReadAsSingleIndexDataFrame(self): 

1775 df1, allColumns = _makeSingleIndexDataFrame() 

1776 

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

1778 

1779 # Read back out as a dataframe. 

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

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

1782 

1783 # Read back out as an arrow table, convert to dataframe. 

1784 tab3 = self.butler.get(self.datasetType, dataId={}) 

1785 df3 = arrow_to_pandas(tab3) 

1786 self.assertTrue(df1.equals(df3)) 

1787 

1788 # Check reading the columns. 

1789 columns = df2.reset_index().columns 

1790 columns2 = self.butler.get( 

1791 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex" 

1792 ) 

1793 # We check the set because pandas reorders the columns. 

1794 self.assertEqual(set(columns2.to_list()), set(columns.to_list())) 

1795 

1796 # Check reading the schema. 

1797 schema = DataFrameSchema(df1) 

1798 schema2 = self.butler.get( 

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

1800 ) 

1801 self.assertEqual(schema2, schema) 

1802 

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

1804 def testWriteArrowTableReadAsMultiIndexDataFrame(self): 

1805 df1 = _makeMultiIndexDataFrame() 

1806 

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

1808 

1809 # Read back out as a dataframe. 

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

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

1812 

1813 # Read back out as an arrow table, convert to dataframe. 

1814 atab3 = self.butler.get(self.datasetType, dataId={}) 

1815 df3 = arrow_to_pandas(atab3) 

1816 self.assertTrue(df1.equals(df3)) 

1817 

1818 # Check reading the columns. 

1819 columns = df2.columns 

1820 columns2 = self.butler.get( 

1821 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="DataFrameIndex" 

1822 ) 

1823 self.assertTrue(columns2.equals(columns)) 

1824 

1825 # Check reading the schema. 

1826 schema = DataFrameSchema(df1) 

1827 schema2 = self.butler.get( 

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

1829 ) 

1830 self.assertEqual(schema2, schema) 

1831 

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

1833 def testWriteArrowTableReadAsAstropyTable(self): 

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

1835 

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

1837 

1838 # Read back out as an astropy table. 

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

1840 _checkAstropyTableEquality(tab1, tab2) 

1841 

1842 # Read back out as an arrow table, convert to astropy table. 

1843 atab3 = self.butler.get(self.datasetType, dataId={}) 

1844 tab3 = arrow_to_astropy(atab3) 

1845 _checkAstropyTableEquality(tab1, tab3) 

1846 

1847 # Check reading the columns. 

1848 columns = list(tab2.columns.keys()) 

1849 columns2 = self.butler.get( 

1850 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList" 

1851 ) 

1852 self.assertEqual(columns2, columns) 

1853 

1854 # Check reading the schema. 

1855 schema = ArrowAstropySchema(tab1) 

1856 schema2 = self.butler.get( 

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

1858 ) 

1859 self.assertEqual(schema2, schema) 

1860 

1861 # Check the schema conversions and units. 

1862 arrow_schema = schema.to_arrow_schema() 

1863 for name in arrow_schema.names: 

1864 field_metadata = arrow_schema.field(name).metadata 

1865 if ( 

1866 b"description" in field_metadata 

1867 and (description := field_metadata[b"description"].decode("UTF-8")) != "" 

1868 ): 

1869 self.assertEqual(schema2.schema[name].description, description) 

1870 else: 

1871 self.assertIsNone(schema2.schema[name].description) 

1872 if b"unit" in field_metadata and (unit := field_metadata[b"unit"].decode("UTF-8")) != "": 

1873 self.assertEqual(schema2.schema[name].unit, units.Unit(unit)) 

1874 

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

1876 def testWriteArrowTableReadAsNumpyTable(self): 

1877 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1878 

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

1880 

1881 # Read back out as a numpy table. 

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

1883 _checkNumpyTableEquality(tab1, tab2) 

1884 

1885 # Read back out as an arrow table, convert to numpy table. 

1886 atab3 = self.butler.get(self.datasetType, dataId={}) 

1887 tab3 = arrow_to_numpy(atab3) 

1888 _checkNumpyTableEquality(tab1, tab3) 

1889 

1890 # Check reading the columns. 

1891 columns = list(tab2.dtype.names) 

1892 columns2 = self.butler.get( 

1893 self.datasetType.componentTypeName("columns"), dataId={}, storageClass="ArrowColumnList" 

1894 ) 

1895 self.assertEqual(columns2, columns) 

1896 

1897 # Check reading the schema. 

1898 schema = ArrowNumpySchema(tab1.dtype) 

1899 schema2 = self.butler.get( 

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

1901 ) 

1902 self.assertEqual(schema2, schema) 

1903 

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

1905 def testWriteArrowTableReadAsNumpyDict(self): 

1906 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1907 

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

1909 

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

1911 tab2_numpy = _numpy_dict_to_numpy(tab2) 

1912 _checkNumpyTableEquality(tab1, tab2_numpy) 

1913 

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

1915 def testWriteReadAstropyTableLossless(self): 

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

1917 

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

1919 

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

1921 

1922 _checkAstropyTableEquality(tab1, tab2) 

1923 

1924 

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

1926class InMemoryArrowTableDelegateTestCase(ParquetFormatterArrowTableTestCase): 

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

1928 

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

1930 

1931 def testBadInput(self): 

1932 tab1 = _makeSimpleArrowTable() 

1933 delegate = ArrowTableDelegate("ArrowTable") 

1934 

1935 with self.assertRaises(ValueError): 

1936 delegate.handleParameters(inMemoryDataset="not_an_arrow_table") 

1937 

1938 with self.assertRaises(NotImplementedError): 

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

1940 

1941 with self.assertRaises(AttributeError): 

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

1943 

1944 def testStorageClass(self): 

1945 tab1 = _makeSimpleArrowTable() 

1946 

1947 factory = StorageClassFactory() 

1948 factory.addFromConfig(StorageClassConfig()) 

1949 

1950 storageClass = factory.findStorageClass(type(tab1), compare_types=False) 

1951 # Force the name lookup to do name matching. 

1952 storageClass._pytype = None 

1953 self.assertEqual(storageClass.name, "ArrowTable") 

1954 

1955 storageClass = factory.findStorageClass(type(tab1), compare_types=True) 

1956 # Force the name lookup to do name matching. 

1957 storageClass._pytype = None 

1958 self.assertEqual(storageClass.name, "ArrowTable") 

1959 

1960 

1961@unittest.skipUnless(np is not None, "Cannot test ParquetFormatterArrowNumpy without numpy.") 

1962@unittest.skipUnless(pa is not None, "Cannot test ParquetFormatterArrowNumpy without pyarrow.") 

1963class ParquetFormatterArrowNumpyDictTestCase(unittest.TestCase): 

1964 """Tests for ParquetFormatter, ArrowNumpyDict, using local file store.""" 

1965 

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

1967 

1968 def setUp(self): 

1969 """Create a new butler root for each test.""" 

1970 self.root = makeTestTempDir(TESTDIR) 

1971 config = Config(self.configFile) 

1972 self.butler = Butler.from_config( 

1973 make_repo_for_test(self.root, config=config), writeable=True, run="test_run" 

1974 ) 

1975 self.enterContext(self.butler) 

1976 # No dimensions in dataset type so we don't have to worry about 

1977 # inserting dimension data or defining data IDs. 

1978 self.datasetType = DatasetType( 

1979 "data", dimensions=(), storageClass="ArrowNumpyDict", universe=self.butler.dimensions 

1980 ) 

1981 self.butler.registry.registerDatasetType(self.datasetType) 

1982 

1983 def tearDown(self): 

1984 removeTestTempDir(self.root) 

1985 

1986 def testNumpyDict(self): 

1987 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

1988 dict1 = _numpy_to_numpy_dict(tab1) 

1989 

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

1991 # Read the whole table. 

1992 dict2 = self.butler.get(self.datasetType, dataId={}) 

1993 _checkNumpyDictEquality(dict1, dict2) 

1994 # Read the columns. 

1995 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={}) 

1996 self.assertEqual(len(columns2), len(dict1.keys())) 

1997 for name in dict1: 

1998 self.assertIn(name, columns2) 

1999 # Read the rowcount. 

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

2001 self.assertEqual(rowcount, len(dict1["a"])) 

2002 # Read the schema. 

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

2004 self.assertEqual(schema, ArrowNumpySchema(tab1.dtype)) 

2005 # Read just some columns a few different ways. 

2006 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]}) 

2007 subdict = {key: dict1[key] for key in ["a", "c"]} 

2008 _checkNumpyDictEquality(subdict, tab3) 

2009 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"}) 

2010 subdict = {key: dict1[key] for key in ["a"]} 

2011 _checkNumpyDictEquality(subdict, tab4) 

2012 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]}) 

2013 subdict = {key: dict1[key] for key in ["index", "a"]} 

2014 _checkNumpyDictEquality(subdict, tab5) 

2015 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"}) 

2016 subdict = {key: dict1[key] for key in ["ddd"]} 

2017 _checkNumpyDictEquality(subdict, tab6) 

2018 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]}) 

2019 subdict = {key: dict1[key] for key in ["a"]} 

2020 _checkNumpyDictEquality(subdict, tab7) 

2021 tab8 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["d??", "a*"]}) 

2022 subdict = {key: dict1[key] for key in ["ddd", "dtn", "dtu", "a"]} 

2023 _checkNumpyDictEquality(subdict, tab8) 

2024 # Passing an unrecognized column should be a ValueError. 

2025 with self.assertRaises(ValueError): 

2026 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]}) 

2027 

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

2029 def testWriteNumpyDictReadAsArrowTable(self): 

2030 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

2031 dict1 = _numpy_to_numpy_dict(tab1) 

2032 

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

2034 

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

2036 

2037 tab2_dict = arrow_to_numpy_dict(tab2) 

2038 

2039 _checkNumpyDictEquality(dict1, tab2_dict) 

2040 

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

2042 def testWriteNumpyDictReadAsDataFrame(self): 

2043 tab1 = _makeSimpleNumpyTable() 

2044 dict1 = _numpy_to_numpy_dict(tab1) 

2045 

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

2047 

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

2049 

2050 # The order of the dict may get mixed up, so we need to check column 

2051 # by column. We also need to do this in dataframe form because pandas 

2052 # changes the datatype of the string column. 

2053 tab1_df = pd.DataFrame(tab1) 

2054 

2055 self.assertEqual(set(tab1_df.columns), set(tab2.columns)) 

2056 for col in tab1_df.columns: 

2057 self.assertTrue(np.all(tab1_df[col].values == tab2[col].values)) 

2058 

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

2060 def testWriteNumpyDictReadAsAstropyTable(self): 

2061 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

2062 dict1 = _numpy_to_numpy_dict(tab1) 

2063 

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

2065 

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

2067 tab2_dict = _astropy_to_numpy_dict(tab2) 

2068 

2069 _checkNumpyDictEquality(dict1, tab2_dict) 

2070 

2071 def testWriteNumpyDictReadAsNumpyTable(self): 

2072 tab1 = _makeSimpleNumpyTable(include_multidim=True) 

2073 dict1 = _numpy_to_numpy_dict(tab1) 

2074 

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

2076 

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

2078 tab2_dict = _numpy_to_numpy_dict(tab2) 

2079 

2080 _checkNumpyDictEquality(dict1, tab2_dict) 

2081 

2082 def testWriteNumpyDictBad(self): 

2083 dict1 = {"a": 4, "b": np.ndarray([1])} 

2084 with self.assertRaises(RuntimeError): 

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

2086 

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

2088 with self.assertRaises(RuntimeError): 

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

2090 

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

2092 with self.assertRaises(RuntimeError): 

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

2094 

2095 dict4 = {"a": np.zeros(4), "b": np.zeros(4, dtype="O")} 

2096 with self.assertRaises(RuntimeError): 

2097 self.butler.put(dict4, self.datasetType, dataId={}) 

2098 

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

2100 def testWriteReadAstropyTableLossless(self): 

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

2102 

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

2104 

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

2106 

2107 _checkAstropyTableEquality(tab1, tab2) 

2108 

2109 

2110@unittest.skipUnless(np is not None, "Cannot test InMemoryDatastore with NumpyDict without numpy.") 

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

2112class InMemoryNumpyDictDelegateTestCase(ParquetFormatterArrowNumpyDictTestCase): 

2113 """Tests for InMemoryDatastore, using ArrowTableDelegate with 

2114 Numpy dict. 

2115 """ 

2116 

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

2118 

2119 def testWriteNumpyDictBad(self): 

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

2121 pass 

2122 

2123 

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

2125class ParquetFormatterArrowSchemaTestCase(unittest.TestCase): 

2126 """Tests for ParquetFormatter, ArrowSchema, using local file datastore.""" 

2127 

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

2129 

2130 def setUp(self): 

2131 """Create a new butler root for each test.""" 

2132 self.root = makeTestTempDir(TESTDIR) 

2133 config = Config(self.configFile) 

2134 self.butler = Butler.from_config( 

2135 make_repo_for_test(self.root, config=config), writeable=True, run="test_run" 

2136 ) 

2137 self.enterContext(self.butler) 

2138 # No dimensions in dataset type so we don't have to worry about 

2139 # inserting dimension data or defining data IDs. 

2140 self.datasetType = DatasetType( 

2141 "data", dimensions=(), storageClass="ArrowSchema", universe=self.butler.dimensions 

2142 ) 

2143 self.butler.registry.registerDatasetType(self.datasetType) 

2144 

2145 def tearDown(self): 

2146 removeTestTempDir(self.root) 

2147 

2148 def _makeTestSchema(self): 

2149 schema = pa.schema( 

2150 [ 

2151 pa.field( 

2152 "int32", 

2153 pa.int32(), 

2154 nullable=False, 

2155 metadata={ 

2156 "description": "32-bit integer", 

2157 "unit": "", 

2158 }, 

2159 ), 

2160 pa.field( 

2161 "int64", 

2162 pa.int64(), 

2163 nullable=False, 

2164 metadata={ 

2165 "description": "64-bit integer", 

2166 "unit": "", 

2167 }, 

2168 ), 

2169 pa.field( 

2170 "uint64", 

2171 pa.uint64(), 

2172 nullable=False, 

2173 metadata={ 

2174 "description": "64-bit unsigned integer", 

2175 "unit": "", 

2176 }, 

2177 ), 

2178 pa.field( 

2179 "float32", 

2180 pa.float32(), 

2181 nullable=False, 

2182 metadata={ 

2183 "description": "32-bit float", 

2184 "unit": "count", 

2185 }, 

2186 ), 

2187 pa.field( 

2188 "float64", 

2189 pa.float64(), 

2190 nullable=False, 

2191 metadata={ 

2192 "description": "64-bit float", 

2193 "unit": "nJy", 

2194 }, 

2195 ), 

2196 pa.field( 

2197 "fixed_size_list", 

2198 pa.list_(pa.float64(), list_size=10), 

2199 nullable=False, 

2200 metadata={ 

2201 "description": "Fixed size list of 64-bit floats.", 

2202 "unit": "nJy", 

2203 }, 

2204 ), 

2205 pa.field( 

2206 "variable_size_list", 

2207 pa.list_(pa.float64()), 

2208 nullable=False, 

2209 metadata={ 

2210 "description": "Variable size list of 64-bit floats.", 

2211 "unit": "nJy", 

2212 }, 

2213 ), 

2214 # One of these fields will have no description. 

2215 pa.field( 

2216 "string", 

2217 pa.string(), 

2218 nullable=False, 

2219 metadata={ 

2220 "unit": "", 

2221 }, 

2222 ), 

2223 # One of these fields will have no metadata. 

2224 pa.field( 

2225 "binary", 

2226 pa.binary(), 

2227 nullable=False, 

2228 ), 

2229 ] 

2230 ) 

2231 

2232 return schema 

2233 

2234 def testArrowSchema(self): 

2235 schema1 = self._makeTestSchema() 

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

2237 

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

2239 self.assertEqual(schema2, schema1) 

2240 

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

2242 def testWriteArrowSchemaReadAsDataFrameSchema(self): 

2243 schema1 = self._makeTestSchema() 

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

2245 

2246 df_schema1 = DataFrameSchema.from_arrow(schema1) 

2247 

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

2249 self.assertEqual(df_schema2, df_schema1) 

2250 

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

2252 def testWriteArrowSchemaReadAsArrowAstropySchema(self): 

2253 schema1 = self._makeTestSchema() 

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

2255 

2256 ap_schema1 = ArrowAstropySchema.from_arrow(schema1) 

2257 

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

2259 self.assertEqual(ap_schema2, ap_schema1) 

2260 

2261 # Confirm that the ap_schema2 has the unit/description we expect. 

2262 for name in schema1.names: 

2263 field_metadata = schema1.field(name).metadata 

2264 if field_metadata is None: 

2265 continue 

2266 if ( 

2267 b"description" in field_metadata 

2268 and (description := field_metadata[b"description"].decode("UTF-8")) != "" 

2269 ): 

2270 self.assertEqual(ap_schema2.schema[name].description, description) 

2271 else: 

2272 self.assertIsNone(ap_schema2.schema[name].description) 

2273 if b"unit" in field_metadata and (unit := field_metadata[b"unit"].decode("UTF-8")) != "": 

2274 self.assertEqual(ap_schema2.schema[name].unit, units.Unit(unit)) 

2275 

2276 @unittest.skipUnless(atable is not None, "Cannot test reading as an numpy schema without numpy.") 

2277 def testWriteArrowSchemaReadAsArrowNumpySchema(self): 

2278 schema1 = self._makeTestSchema() 

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

2280 

2281 np_schema1 = ArrowNumpySchema.from_arrow(schema1) 

2282 

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

2284 self.assertEqual(np_schema2, np_schema1) 

2285 

2286 

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

2288class InMemoryArrowSchemaDelegateTestCase(ParquetFormatterArrowSchemaTestCase): 

2289 """Tests for InMemoryDatastore and ArrowSchema.""" 

2290 

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

2292 

2293 

2294@unittest.skipUnless(pa is not None, "Cannot test remote datastore without pyarrow.") 

2295@unittest.skipUnless(fsspec is not None, "Cannot test remote datastore without fsspec.") 

2296class ParquetFormatterArrowTableRemoteTestCase(unittest.TestCase): 

2297 """Tests for arrow table/parquet with a datastore that reports itself as 

2298 not local. 

2299 """ 

2300 

2301 # Code is adapted from test_butler.py 

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

2303 

2304 def setUp(self): 

2305 config = Config(self.configFile) 

2306 

2307 self.root = makeTestTempDir(TESTDIR) 

2308 # The space in the directory name is deliberate, to exercise URI 

2309 # percent-encoding. 

2310 root_path = os.path.join(self.root, "butler root") 

2311 os.makedirs(root_path) 

2312 rooturi = make_remote_test_uri(root_path) 

2313 config.update({"datastore": {"datastore": {"root": str(rooturi)}}}) 

2314 

2315 # The registry database has to live on a real local file system. 

2316 self.reg_dir = makeTestTempDir(TESTDIR) 

2317 config["registry", "db"] = f"sqlite:///{self.reg_dir}/gen3.sqlite3" 

2318 

2319 make_repo_for_test(rooturi, config=config, forceConfigRoot=False) 

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

2321 

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

2323 self.enterContext(self.butler) 

2324 

2325 # No dimensions in dataset type so we don't have to worry about 

2326 # inserting dimension data or defining data IDs. 

2327 self.datasetType = DatasetType( 

2328 "data", dimensions=(), storageClass="ArrowTable", universe=self.butler.dimensions 

2329 ) 

2330 self.butler.registry.registerDatasetType(self.datasetType) 

2331 

2332 def tearDown(self): 

2333 removeTestTempDir(self.reg_dir) 

2334 removeTestTempDir(self.root) 

2335 

2336 def testArrowTableRemote(self): 

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

2338 

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

2340 

2341 # Read the whole Table. 

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

2343 # We convert to use the numpy testing framework to handle nan 

2344 # comparisons. 

2345 self.assertEqual(tab1.schema, tab2.schema) 

2346 tab1_np = arrow_to_numpy(tab1) 

2347 tab2_np = arrow_to_numpy(tab2) 

2348 for col in tab1.column_names: 

2349 np.testing.assert_array_equal(tab2_np[col], tab1_np[col]) 

2350 # Read the columns. 

2351 columns2 = self.butler.get(self.datasetType.componentTypeName("columns"), dataId={}) 

2352 self.assertEqual(len(columns2), len(tab1.schema.names)) 

2353 for i, name in enumerate(tab1.schema.names): 

2354 self.assertEqual(columns2[i], name) 

2355 # Read the rowcount. 

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

2357 self.assertEqual(rowcount, len(tab1)) 

2358 # Read the schema. 

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

2360 self.assertEqual(schema, tab1.schema) 

2361 # Read just some columns a few different ways. 

2362 tab3 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "c"]}) 

2363 self.assertEqual(tab3, tab1.select(("a", "c"))) 

2364 tab4 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "a"}) 

2365 self.assertEqual(tab4, tab1.select(("a",))) 

2366 tab5 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["index", "a"]}) 

2367 self.assertEqual(tab5, tab1.select(("index", "a"))) 

2368 tab6 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": "ddd"}) 

2369 self.assertEqual(tab6, tab1.select(("ddd",))) 

2370 tab7 = self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["a", "a"]}) 

2371 self.assertEqual(tab7, tab1.select(("a",))) 

2372 # Passing an unrecognized column should be a ValueError. 

2373 with self.assertRaises(ValueError): 

2374 self.butler.get(self.datasetType, dataId={}, parameters={"columns": ["e"]}) 

2375 

2376 

2377@unittest.skipUnless(np is not None, "Cannot test compute_row_group_size without numpy.") 

2378@unittest.skipUnless(pa is not None, "Cannot test compute_row_group_size without pyarrow.") 

2379class ComputeRowGroupSizeTestCase(unittest.TestCase): 

2380 """Tests for compute_row_group_size.""" 

2381 

2382 def testRowGroupSizeNoMetadata(self): 

2383 numpyTable = _makeSimpleNumpyTable(include_multidim=True) 

2384 

2385 # We can't use the numpy_to_arrow convenience function because 

2386 # that adds metadata. 

2387 type_list = _numpy_dtype_to_arrow_types(numpyTable.dtype) 

2388 schema = pa.schema(type_list) 

2389 arrays = _numpy_style_arrays_to_arrow_arrays( 

2390 numpyTable.dtype, 

2391 len(numpyTable), 

2392 numpyTable, 

2393 schema, 

2394 ) 

2395 arrowTable = pa.Table.from_arrays(arrays, schema=schema) 

2396 

2397 row_group_size = compute_row_group_size(arrowTable.schema) 

2398 

2399 self.assertGreater(row_group_size, 1_000_000) 

2400 self.assertLess(row_group_size, 2_000_000) 

2401 

2402 def testRowGroupSizeWithMetadata(self): 

2403 numpyTable = _makeSimpleNumpyTable(include_multidim=True) 

2404 

2405 arrowTable = numpy_to_arrow(numpyTable) 

2406 

2407 row_group_size = compute_row_group_size(arrowTable.schema) 

2408 

2409 self.assertGreater(row_group_size, 1_000_000) 

2410 self.assertLess(row_group_size, 2_000_000) 

2411 

2412 def testRowGroupSizeTinyTable(self): 

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

2414 

2415 arrowTable = numpy_to_arrow(numpyTable) 

2416 

2417 row_group_size = compute_row_group_size(arrowTable.schema) 

2418 

2419 self.assertGreater(row_group_size, 1_000_000) 

2420 

2421 @unittest.skipUnless(pd is not None, "Cannot run testRowGroupSizeDataFrameWithLists without pandas.") 

2422 def testRowGroupSizeDataFrameWithLists(self): 

2423 df = pd.DataFrame({"a": np.zeros(10), "b": [[0, 0]] * 10, "c": [[0.0, 0.0]] * 10, "d": [[]] * 10}) 

2424 arrowTable = pandas_to_arrow(df) 

2425 row_group_size = compute_row_group_size(arrowTable.schema) 

2426 

2427 self.assertGreater(row_group_size, 1_000_000) 

2428 

2429 

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

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

2432 

2433 Parameters 

2434 ---------- 

2435 table1 : `astropy.table.Table` 

2436 table2 : `astropy.table.Table` 

2437 skip_units : `bool` 

2438 has_bigendian : `bool` 

2439 """ 

2440 if not has_bigendian: 

2441 assert table1.dtype == table2.dtype 

2442 else: 

2443 for name in table1.dtype.names: 

2444 # Only check type matches, force to little-endian. 

2445 assert table1.dtype[name].newbyteorder(">") == table2.dtype[name].newbyteorder(">") 

2446 

2447 # Strip provenance before comparison. 

2448 DatasetProvenance.strip_provenance_from_flat_dict(table1.meta) 

2449 DatasetProvenance.strip_provenance_from_flat_dict(table2.meta) 

2450 assert table1.meta == table2.meta 

2451 if not skip_units: 

2452 for name in table1.columns: 

2453 assert table1[name].unit == table2[name].unit 

2454 assert table1[name].description == table2[name].description 

2455 assert table1[name].format == table2[name].format 

2456 

2457 for name in table1.columns: 

2458 # We need to check masked/regular columns after filling. 

2459 has_masked = False 

2460 if isinstance(table1[name], atable.column.MaskedColumn): 

2461 c1 = table1[name].filled() 

2462 has_masked = True 

2463 else: 

2464 c1 = np.array(table1[name]) 

2465 if has_masked: 

2466 assert isinstance(table2[name], atable.column.MaskedColumn) 

2467 c2 = table2[name].filled() 

2468 else: 

2469 assert not isinstance(table2[name], atable.column.MaskedColumn) 

2470 c2 = np.array(table2[name]) 

2471 np.testing.assert_array_equal(c1, c2) 

2472 # If we have a masked column then we test the underlying data. 

2473 if has_masked: 

2474 np.testing.assert_array_equal(np.array(c1), np.array(c2)) 

2475 np.testing.assert_array_equal(table1[name].mask, table2[name].mask) 

2476 

2477 

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

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

2480 

2481 Parameters 

2482 ---------- 

2483 table1 : `numpy.ndarray` 

2484 table2 : `numpy.ndarray` 

2485 has_bigendian : `bool` 

2486 """ 

2487 assert table1.dtype.names == table2.dtype.names 

2488 for name in table1.dtype.names: 

2489 if not has_bigendian: 

2490 assert table1.dtype[name] == table2.dtype[name] 

2491 else: 

2492 # Only check type matches, force to little-endian. 

2493 assert table1.dtype[name].newbyteorder(">") == table2.dtype[name].newbyteorder(">") 

2494 assert np.all(table1 == table2) 

2495 

2496 

2497def _checkNumpyDictEquality(dict1, dict2): 

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

2499 

2500 Parameters 

2501 ---------- 

2502 dict1 : `dict` [`str`, `np.ndarray`] 

2503 dict2 : `dict` [`str`, `np.ndarray`] 

2504 """ 

2505 assert set(dict1.keys()) == set(dict2.keys()) 

2506 for name in dict1: 

2507 assert dict1[name].dtype == dict2[name].dtype 

2508 assert np.all(dict1[name] == dict2[name]) 

2509 

2510 

2511if __name__ == "__main__": 

2512 unittest.main()