Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/io/pytables.py: 17%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

2566 statements  

1""" 

2High level interface to PyTables for reading and writing pandas data structures 

3to disk 

4""" 

5 

6from __future__ import annotations 

7 

8from contextlib import suppress 

9import copy 

10from datetime import ( 

11 date, 

12 tzinfo, 

13) 

14import itertools 

15import os 

16import re 

17from textwrap import dedent 

18from typing import ( 

19 TYPE_CHECKING, 

20 Any, 

21 Final, 

22 Literal, 

23 Self, 

24 TypeAlias, 

25 cast, 

26 overload, 

27) 

28import warnings 

29 

30import numpy as np 

31 

32from pandas._config import ( 

33 using_string_dtype, 

34) 

35import pandas._config.config as cf 

36from pandas._config.config import _global_config as config 

37 

38from pandas._libs import ( 

39 lib, 

40 writers as libwriters, 

41) 

42from pandas._libs.lib import is_string_array 

43from pandas._libs.tslibs import timezones 

44from pandas.compat import ( 

45 HAS_PYARROW, 

46 PY312, 

47) 

48from pandas.compat._optional import import_optional_dependency 

49from pandas.compat.pickle_compat import patch_pickle 

50from pandas.errors import ( 

51 AttributeConflictWarning, 

52 ClosedFileError, 

53 Pandas4Warning, 

54 PerformanceWarning, 

55 PossibleDataLossError, 

56) 

57from pandas.util._decorators import ( 

58 cache_readonly, 

59 set_module, 

60) 

61from pandas.util._exceptions import find_stack_level 

62 

63from pandas.core.dtypes.common import ( 

64 ensure_object, 

65 is_bool_dtype, 

66 is_complex_dtype, 

67 is_list_like, 

68 is_string_dtype, 

69 needs_i8_conversion, 

70) 

71from pandas.core.dtypes.dtypes import ( 

72 CategoricalDtype, 

73 DatetimeTZDtype, 

74 ExtensionDtype, 

75 PeriodDtype, 

76) 

77from pandas.core.dtypes.missing import array_equivalent 

78 

79from pandas import ( 

80 CategoricalIndex, 

81 DataFrame, 

82 DatetimeIndex, 

83 Index, 

84 MultiIndex, 

85 PeriodIndex, 

86 RangeIndex, 

87 Series, 

88 StringDtype, 

89 TimedeltaIndex, 

90 concat, 

91 isna, 

92) 

93from pandas.core.arrays import ( 

94 Categorical, 

95 DatetimeArray, 

96 PeriodArray, 

97) 

98from pandas.core.arrays.datetimes import tz_to_dtype 

99from pandas.core.arrays.string_ import BaseStringArray 

100import pandas.core.common as com 

101from pandas.core.computation.pytables import ( 

102 JointConditionBinOp, 

103 PyTablesExpr, 

104 maybe_expression, 

105) 

106from pandas.core.construction import ( 

107 array as pd_array, 

108 extract_array, 

109) 

110from pandas.core.indexes.api import ensure_index 

111 

112from pandas.io.common import stringify_path 

113from pandas.io.formats.printing import ( 

114 adjoin, 

115 pprint_thing, 

116) 

117 

118if TYPE_CHECKING: 

119 from collections.abc import ( 

120 Callable, 

121 Hashable, 

122 Iterator, 

123 ) 

124 from types import ( 

125 ModuleType, 

126 TracebackType, 

127 ) 

128 

129 from tables import ( 

130 Col, 

131 File, 

132 Node, 

133 ) 

134 

135 from pandas._typing import ( 

136 AnyArrayLike, 

137 ArrayLike, 

138 AxisInt, 

139 DtypeArg, 

140 FilePath, 

141 TimeUnit, 

142 npt, 

143 ) 

144 

145 from pandas.core.internals import Block 

146 

147 

148# encoding 

149_default_encoding = "UTF-8" 

150 

151 

152def _ensure_encoding(encoding: str | None) -> str: 

153 # set the encoding if we need 

154 if encoding is None: 

155 encoding = _default_encoding 

156 

157 return encoding 

158 

159 

160def _ensure_str(name): 

161 """ 

162 Ensure that an index / column name is a str (python 3); otherwise they 

163 may be np.string dtype. Non-string dtypes are passed through unchanged. 

164 

165 https://github.com/pandas-dev/pandas/issues/13492 

166 """ 

167 if isinstance(name, str): 

168 name = str(name) 

169 return name 

170 

171 

172Term: TypeAlias = PyTablesExpr 

173 

174 

175def _ensure_term(where, scope_level: int): 

176 """ 

177 Ensure that the where is a Term or a list of Term. 

178 

179 This makes sure that we are capturing the scope of variables that are 

180 passed create the terms here with a frame_level=2 (we are 2 levels down) 

181 """ 

182 # only consider list/tuple here as an ndarray is automatically a coordinate 

183 # list 

184 level = scope_level + 1 

185 if isinstance(where, (list, tuple)): 

186 # Pre-3.12 the list comprehension created its own stack frame, 

187 # requiring an extra +1 to reach the caller's scope. PEP 709 

188 # inlined comprehensions in 3.12, removing that frame. 

189 comp_adjustment = 0 if PY312 else 1 

190 where = [ 

191 Term(term, scope_level=level + comp_adjustment) 

192 if maybe_expression(term) 

193 else term 

194 for term in where 

195 if term is not None 

196 ] 

197 elif maybe_expression(where): 

198 where = Term(where, scope_level=level) 

199 return where if where is None or len(where) else None 

200 

201 

202attribute_conflict_doc: Final = """ 

203the [%s] attribute of the existing index is [%s] which conflicts with the new 

204[%s], resetting the attribute to None 

205""" 

206 

207performance_doc: Final = """ 

208your performance may suffer as PyTables will pickle object types that it cannot 

209map directly to c-types [inferred_type->%s,key->%s] [items->%s] 

210""" 

211 

212# formats 

213_FORMAT_MAP = {"f": "fixed", "fixed": "fixed", "t": "table", "table": "table"} 

214 

215# axes map 

216_AXES_MAP = {DataFrame: [0]} 

217 

218# register our configuration options 

219dropna_doc: Final = """ 

220: boolean 

221 drop ALL nan rows when appending to a table 

222""" 

223format_doc: Final = """ 

224: format 

225 default format writing format, if None, then 

226 put will default to 'fixed' and append will default to 'table' 

227""" 

228 

229with cf.config_prefix("io.hdf"): 

230 cf.register_option("dropna_table", False, dropna_doc, validator=cf.is_bool) 

231 cf.register_option( 

232 "default_format", 

233 None, 

234 format_doc, 

235 validator=cf.is_one_of_factory(["fixed", "table", None]), 

236 ) 

237 

238cf.deprecate_option( 

239 "io.hdf.dropna_table", 

240 Pandas4Warning, 

241 msg="io.hdf.dropna_table option is deprecated. Use DataFrame.dropna " 

242 "before writing instead.", 

243) 

244 

245# oh the troubles to reduce import time 

246_table_mod: ModuleType | None = None 

247_table_file_open_policy_is_strict = False 

248 

249 

250_MISSING = object() 

251 

252 

253def _set_attr_if_changed(attrs, name: str, value) -> None: 

254 """ 

255 setattr on a PyTables AttributeSet only if the on-disk value differs. 

256 

257 Re-writing an HDF5 attribute to the same value is expensive — pytables 

258 deletes and re-creates it, hitting the disk per attribute. On wide-table 

259 appends this dominates runtime (GH#25839). 

260 """ 

261 current = getattr(attrs, name, _MISSING) 

262 if current is _MISSING: 

263 setattr(attrs, name, value) 

264 return 

265 try: 

266 equal = bool(current == value) 

267 except (ValueError, TypeError): 

268 equal = False 

269 if not equal: 

270 setattr(attrs, name, value) 

271 

272 

273def _resolve_row_window( 

274 start: int | None, stop: int | None, nrows: int 

275) -> tuple[int, int]: 

276 """ 

277 Resolve ``start``/``stop`` row bounds against a table holding ``nrows`` rows. 

278 

279 Slice semantics count a negative bound back from the end of the table *and* 

280 clamp both bounds into ``[0, nrows]``, which is what a plain read does; the 

281 row-coordinate paths have to agree with it. 

282 """ 

283 start, stop, _ = slice(start, stop).indices(nrows) 

284 return start, stop 

285 

286 

287def _tables(): 

288 global _table_mod 

289 global _table_file_open_policy_is_strict 

290 if _table_mod is None: 

291 import tables 

292 

293 _table_mod = tables 

294 

295 # set the file open policy 

296 # return the file open policy; this changes as of pytables 3.1 

297 # depending on the HDF5 version 

298 with suppress(AttributeError): 

299 _table_file_open_policy_is_strict = ( 

300 tables.file._FILE_OPEN_POLICY == "strict" 

301 ) 

302 

303 return _table_mod 

304 

305 

306# interface to/from ### 

307 

308 

309def to_hdf( 

310 path_or_buf: FilePath | HDFStore, 

311 key: str, 

312 value: DataFrame | Series, 

313 mode: str = "a", 

314 complevel: int | None = None, 

315 complib: str | None = None, 

316 append: bool = False, 

317 format: str | None = None, 

318 index: bool = True, 

319 min_itemsize: int | dict[str, int] | None = None, 

320 nan_rep=None, 

321 dropna: bool | lib.NoDefault | None = lib.no_default, 

322 data_columns: Literal[True] | list[str] | None = None, 

323 errors: str = "strict", 

324 encoding: str = "UTF-8", 

325) -> None: 

326 """store this object, close it if we opened it""" 

327 if append: 

328 f = lambda store: store.append( 

329 key, 

330 value, 

331 format=format, 

332 index=index, 

333 min_itemsize=min_itemsize, 

334 nan_rep=nan_rep, 

335 dropna=dropna, 

336 data_columns=data_columns, 

337 errors=errors, 

338 encoding=encoding, 

339 ) 

340 else: 

341 # NB: dropna is not passed to `put` 

342 f = lambda store: store.put( 

343 key, 

344 value, 

345 format=format, 

346 index=index, 

347 min_itemsize=min_itemsize, 

348 nan_rep=nan_rep, 

349 data_columns=data_columns, 

350 errors=errors, 

351 encoding=encoding, 

352 dropna=dropna, 

353 track_times=False, 

354 ) 

355 

356 if isinstance(path_or_buf, HDFStore): 

357 f(path_or_buf) 

358 else: 

359 path_or_buf = stringify_path(path_or_buf) 

360 with HDFStore( 

361 path_or_buf, mode=mode, complevel=complevel, complib=complib 

362 ) as store: 

363 f(store) 

364 

365 

366@set_module("pandas") 

367def read_hdf( 

368 path_or_buf: FilePath | HDFStore, 

369 key=None, 

370 mode: str = "r", 

371 errors: str = "strict", 

372 where: str | list | None = None, 

373 start: int | None = None, 

374 stop: int | None = None, 

375 columns: list[str] | None = None, 

376 iterator: bool = False, 

377 chunksize: int | None = None, 

378 **kwargs, 

379): 

380 """ 

381 Read from the store, close it if we opened it. 

382 

383 Retrieve pandas object stored in file, optionally based on where 

384 criteria. This function requires the 

385 `PyTables <https://www.pytables.org/>`_ library. 

386 

387 .. note:: 

388 

389 This function only reads HDF5 files written by pandas (via 

390 :meth:`DataFrame.to_hdf`, :meth:`Series.to_hdf`, or :class:`HDFStore`), 

391 which use a pandas-specific layout built on PyTables. Arbitrary HDF5 

392 files produced by other tools such as ``h5py`` or plain PyTables are 

393 not supported; use those libraries directly to read such files. 

394 

395 .. warning:: 

396 

397 Pandas uses PyTables for reading and writing HDF5 files, which allows 

398 serializing object-dtype data with pickle when using the "fixed" format. 

399 Loading pickled data received from untrusted sources can be unsafe. 

400 

401 See: https://docs.python.org/3/library/pickle.html for more. 

402 

403 Parameters 

404 ---------- 

405 path_or_buf : str, path object, pandas.HDFStore 

406 Any valid string path is acceptable. Only supports the local file system, 

407 remote URLs and file-like objects are not supported. 

408 

409 If you want to pass in a path object, pandas accepts any 

410 ``os.PathLike``. 

411 

412 Alternatively, pandas accepts an open :class:`pandas.HDFStore` object. 

413 

414 key : object, optional 

415 The group identifier in the store. Can be omitted if the HDF file 

416 contains a single pandas object. 

417 mode : {'r', 'r+', 'a'}, default 'r' 

418 Mode to use when opening the file. Ignored if path_or_buf is a 

419 :class:`pandas.HDFStore`. Default is 'r'. 

420 errors : str, default 'strict' 

421 Specifies how encoding and decoding errors are to be handled. 

422 See the errors argument for :func:`open` for a full list 

423 of options. 

424 where : list, optional 

425 A list of Term (or convertible) objects. 

426 start : int, optional 

427 Row number to start selection. 

428 stop : int, optional 

429 Row number to stop selection. 

430 columns : list, optional 

431 A list of columns names to return. 

432 iterator : bool, optional 

433 Return an iterator object. 

434 chunksize : int, optional 

435 Number of rows to include in an iteration when using an iterator. 

436 **kwargs 

437 Additional keyword arguments passed to HDFStore. 

438 

439 Returns 

440 ------- 

441 object 

442 The selected object. Return type depends on the object stored. 

443 

444 See Also 

445 -------- 

446 DataFrame.to_hdf : Write an HDF file from a DataFrame. 

447 HDFStore : Low-level access to HDF files. 

448 

449 Notes 

450 ----- 

451 When ``errors="surrogatepass"``, ``pd.options.future.infer_string`` is true, 

452 and PyArrow is installed, if a UTF-16 surrogate is encountered when decoding 

453 to UTF-8, the resulting dtype will be 

454 ``pd.StringDtype(storage="python", na_value=np.nan)``. 

455 

456 Examples 

457 -------- 

458 >>> df = pd.DataFrame([[1, 1.0, "a"]], columns=["x", "y", "z"]) # doctest: +SKIP 

459 >>> df.to_hdf("./store.h5", key="data") # doctest: +SKIP 

460 >>> reread = pd.read_hdf("./store.h5") # doctest: +SKIP 

461 """ 

462 if mode not in ["r", "r+", "a"]: 

463 raise ValueError( 

464 f"mode {mode} is not allowed while performing a read. " 

465 f"Allowed modes are r, r+ and a." 

466 ) 

467 # grab the scope 

468 if where is not None: 

469 where = _ensure_term(where, scope_level=1) 

470 

471 if isinstance(path_or_buf, HDFStore): 

472 if not path_or_buf.is_open: 

473 raise OSError("The HDFStore must be open for reading.") 

474 

475 store = path_or_buf 

476 auto_close = False 

477 else: 

478 path_or_buf = stringify_path(path_or_buf) 

479 if not isinstance(path_or_buf, str): 

480 raise NotImplementedError( 

481 "Support for generic buffers has not been implemented." 

482 ) 

483 try: 

484 exists = os.path.exists(path_or_buf) 

485 

486 # if filepath is too long 

487 except (TypeError, ValueError): 

488 exists = False 

489 

490 if not exists: 

491 raise FileNotFoundError(f"File {path_or_buf} does not exist") 

492 

493 store = HDFStore(path_or_buf, mode=mode, errors=errors, **kwargs) 

494 # can't auto open/close if we are using an iterator 

495 # so delegate to the iterator 

496 auto_close = True 

497 

498 read_succeeded = False 

499 try: 

500 if key is None: 

501 groups = store.groups() 

502 if len(groups) == 0: 

503 raise ValueError( 

504 "Dataset(s) incompatible with Pandas data types, " 

505 "not table, or no datasets found in HDF5 file." 

506 ) 

507 candidate_only_group = groups[0] 

508 

509 # For the HDF file to have only one dataset, all other groups 

510 # should then be metadata groups for that candidate group. (This 

511 # assumes that the groups() method enumerates parent groups 

512 # before their children.) 

513 for group_to_check in groups[1:]: 

514 if not _is_metadata_of(group_to_check, candidate_only_group): 

515 raise ValueError( 

516 "key must be provided when HDF5 " 

517 "file contains multiple datasets." 

518 ) 

519 key = candidate_only_group._v_pathname 

520 result = store.select( 

521 key, 

522 where=where, 

523 start=start, 

524 stop=stop, 

525 columns=columns, 

526 iterator=iterator, 

527 chunksize=chunksize, 

528 auto_close=auto_close, 

529 ) 

530 read_succeeded = True 

531 return result 

532 finally: 

533 # If the read failed for any reason, close the store when we were the 

534 # ones who opened it, so the caller can reopen the file (GH#28430). On 

535 # success the store is left as-is: an iterator result needs it open. 

536 if not read_succeeded and not isinstance(path_or_buf, HDFStore): 

537 with suppress(AttributeError): 

538 store.close() 

539 

540 

541def _is_metadata_of(group: Node, parent_group: Node) -> bool: 

542 """Check if a given group is a metadata group for a given parent_group.""" 

543 if group._v_depth <= parent_group._v_depth: 

544 return False 

545 

546 current = group 

547 while current._v_depth > 1: 

548 parent = current._v_parent 

549 if parent == parent_group and current._v_name == "meta": 

550 return True 

551 current = current._v_parent 

552 return False 

553 

554 

555@set_module("pandas") 

556class HDFStore: 

557 """ 

558 Dict-like IO interface for storing pandas objects in PyTables. 

559 

560 Either Fixed or Table format. 

561 

562 .. note:: 

563 

564 ``HDFStore`` uses a pandas-specific layout on top of PyTables and is 

565 intended for round-tripping pandas objects. It cannot read arbitrary 

566 HDF5 files produced by other tools such as ``h5py`` or plain PyTables; 

567 use those libraries directly for general HDF5 interoperability. 

568 

569 .. warning:: 

570 

571 Pandas uses PyTables for reading and writing HDF5 files, which allows 

572 serializing object-dtype data with pickle when using the "fixed" format. 

573 Loading pickled data received from untrusted sources can be unsafe. 

574 

575 See: https://docs.python.org/3/library/pickle.html for more. 

576 

577 Parameters 

578 ---------- 

579 path : str 

580 File path to HDF5 file. 

581 mode : {'a', 'w', 'r', 'r+'}, default 'a' 

582 

583 ``'r'`` 

584 Read-only; no data can be modified. 

585 ``'w'`` 

586 Write; a new file is created (an existing file with the same 

587 name would be deleted). 

588 ``'a'`` 

589 Append; an existing file is opened for reading and writing, 

590 and if the file does not exist it is created. 

591 ``'r+'`` 

592 It is similar to ``'a'``, but the file must already exist. 

593 complevel : int, 0-9, default None 

594 Specifies a compression level for data. 

595 A value of 0 or None disables compression. 

596 complib : {'zlib', 'lzo', 'bzip2', 'blosc'}, default 'zlib' 

597 Specifies the compression library to be used. This has no effect 

598 unless ``complevel`` is set to a value greater than 0; passing 

599 ``complib`` alone emits a ``UserWarning`` and produces an 

600 uncompressed store. 

601 These additional compressors for Blosc are supported 

602 (default if no compressor specified: 'blosc:blosclz'): 

603 {'blosc:blosclz', 'blosc:lz4', 'blosc:lz4hc', 'blosc:snappy', 

604 'blosc:zlib', 'blosc:zstd'}. 

605 Specifying a compression library which is not available issues 

606 a ValueError. 

607 fletcher32 : bool, default False 

608 If applying compression use the fletcher32 checksum. 

609 **kwargs 

610 These parameters will be passed to the PyTables open_file method. 

611 

612 Examples 

613 -------- 

614 >>> bar = pd.DataFrame(np.random.randn(10, 4)) 

615 >>> store = pd.HDFStore("test.h5") 

616 >>> store["foo"] = bar # write to HDF5 

617 >>> bar = store["foo"] # retrieve 

618 >>> store.close() 

619 

620 **Create or load HDF5 file in-memory** 

621 

622 When passing the `driver` option to the PyTables open_file method through 

623 **kwargs, the HDF5 file is loaded or created in-memory and will only be 

624 written when closed: 

625 

626 >>> bar = pd.DataFrame(np.random.randn(10, 4)) 

627 >>> store = pd.HDFStore("test.h5", driver="H5FD_CORE") 

628 >>> store["foo"] = bar 

629 >>> store.close() # only now, data is written to disk 

630 """ 

631 

632 _handle: File | None 

633 _mode: str 

634 

635 def __init__( 

636 self, 

637 path, 

638 mode: str = "a", 

639 complevel: int | None = None, 

640 complib=None, 

641 fletcher32: bool = False, 

642 **kwargs, 

643 ) -> None: 

644 if "format" in kwargs: 

645 raise ValueError("format is not a defined argument for HDFStore") 

646 

647 tables = import_optional_dependency("tables") 

648 

649 if complib is not None and complib not in tables.filters.all_complibs: 

650 raise ValueError( 

651 f"complib only supports {tables.filters.all_complibs} compression." 

652 ) 

653 

654 if complib is None and complevel is not None: 

655 complib = tables.filters.default_complib 

656 

657 if complib is not None and complevel is None: 

658 # GH#29310 complib without complevel does not compress, because 

659 # complevel defaults to 0. Warn rather than silently ignoring the 

660 # requested library. 

661 warnings.warn( 

662 f"complib={complib!r} was specified without complevel; no " 

663 "compression will be applied. Pass complevel (an int in 1-9) " 

664 "to enable compression.", 

665 UserWarning, 

666 stacklevel=find_stack_level(), 

667 ) 

668 

669 self._path = stringify_path(path) 

670 if mode is None: 

671 mode = "a" 

672 self._mode = mode 

673 self._handle = None 

674 self._complevel = complevel if complevel else 0 

675 self._complib = complib 

676 self._fletcher32 = fletcher32 

677 self._filters = None 

678 self.open(mode=mode, **kwargs) 

679 

680 def __fspath__(self) -> str: 

681 return self._path 

682 

683 @property 

684 def root(self): 

685 """return the root node""" 

686 self._check_if_open() 

687 assert self._handle is not None # for mypy 

688 return self._handle.root 

689 

690 @property 

691 def filename(self) -> str: 

692 return self._path 

693 

694 def __getitem__(self, key: str): 

695 return self.get(key) 

696 

697 def __setitem__(self, key: str, value) -> None: 

698 self.put(key, value, track_times=False) 

699 

700 def __delitem__(self, key: str) -> int | None: 

701 return self.remove(key) 

702 

703 def __getattr__(self, name: str): 

704 """allow attribute access to get stores""" 

705 try: 

706 return self.get(name) 

707 except (KeyError, ClosedFileError): 

708 pass 

709 raise AttributeError( 

710 f"'{type(self).__name__}' object has no attribute '{name}'" 

711 ) 

712 

713 def __contains__(self, key: str) -> bool: 

714 """ 

715 check for existence of this key 

716 can match the exact pathname or the pathnm w/o the leading '/' 

717 """ 

718 node = self.get_node(key) 

719 if node is not None: 

720 name = node._v_pathname 

721 if key in (name, name[1:]): 

722 return True 

723 return False 

724 

725 def __len__(self) -> int: 

726 return len(self.groups()) 

727 

728 def __repr__(self) -> str: 

729 pstr = pprint_thing(self._path) 

730 return f"{type(self)}\nFile path: {pstr}\n" 

731 

732 def __enter__(self) -> Self: 

733 return self 

734 

735 def __exit__( 

736 self, 

737 exc_type: type[BaseException] | None, 

738 exc_value: BaseException | None, 

739 traceback: TracebackType | None, 

740 ) -> None: 

741 self.close() 

742 

743 def keys(self, include: str = "pandas") -> list[str]: 

744 """ 

745 Return a list of keys corresponding to objects stored in HDFStore. 

746 

747 The keys are absolute path-names within the HDF5 file hierarchy. 

748 By default only pandas objects are returned, but native HDF5 table 

749 objects can be included as well. 

750 

751 Parameters 

752 ---------- 

753 

754 include : str, default 'pandas' 

755 When kind equals 'pandas' return pandas objects. 

756 When kind equals 'native' return native HDF5 Table objects. 

757 

758 Returns 

759 ------- 

760 list 

761 List of ABSOLUTE path-names (e.g. have the leading '/'). 

762 

763 Raises 

764 ------ 

765 raises ValueError if kind has an illegal value 

766 

767 See Also 

768 -------- 

769 HDFStore.info : Prints detailed information on the store. 

770 HDFStore.get_node : Returns the node with the key. 

771 HDFStore.get_storer : Returns the storer object for a key. 

772 

773 Examples 

774 -------- 

775 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

776 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

777 >>> store.put("data", df) # doctest: +SKIP 

778 >>> store.get("data") # doctest: +SKIP 

779 >>> print(store.keys()) # doctest: +SKIP 

780 ['/data1', '/data2'] 

781 >>> store.close() # doctest: +SKIP 

782 """ 

783 if include == "pandas": 

784 return [n._v_pathname for n in self.groups()] 

785 

786 elif include == "native": 

787 assert self._handle is not None # mypy 

788 return [ 

789 n._v_pathname for n in self._handle.walk_nodes("/", classname="Table") 

790 ] 

791 raise ValueError( 

792 f"`include` should be either 'pandas' or 'native' but is '{include}'" 

793 ) 

794 

795 def __iter__(self) -> Iterator[str]: 

796 return iter(self.keys()) 

797 

798 def items(self) -> Iterator[tuple[str, list]]: 

799 """ 

800 iterate on key->group 

801 """ 

802 for g in self.groups(): 

803 yield g._v_pathname, g 

804 

805 def open(self, mode: str = "a", **kwargs) -> None: 

806 """ 

807 Open the file in the specified mode 

808 

809 Parameters 

810 ---------- 

811 mode : {'a', 'w', 'r', 'r+'}, default 'a' 

812 See HDFStore docstring or tables.open_file for info about modes 

813 **kwargs 

814 These parameters will be passed to the PyTables open_file method. 

815 """ 

816 tables = _tables() 

817 

818 if self._mode != mode: 

819 # if we are changing a write mode to read, ok 

820 if self._mode in ["a", "w"] and mode in ["r", "r+"]: 

821 pass 

822 elif mode in ["w"]: 

823 # this would truncate, raise here 

824 if self.is_open: 

825 raise PossibleDataLossError( 

826 f"Re-opening the file [{self._path}] with mode [{self._mode}] " 

827 "will delete the current file!" 

828 ) 

829 

830 self._mode = mode 

831 

832 # close and reopen the handle 

833 if self.is_open: 

834 self.close() 

835 

836 if self._complevel and self._complevel > 0: 

837 self._filters = _tables().Filters( 

838 self._complevel, self._complib, fletcher32=self._fletcher32 

839 ) 

840 

841 if _table_file_open_policy_is_strict and self.is_open: 

842 msg = ( 

843 "Cannot open HDF5 file, which is already opened, " 

844 "even in read-only mode." 

845 ) 

846 raise ValueError(msg) 

847 

848 self._handle = tables.open_file(self._path, self._mode, **kwargs) 

849 

850 def close(self) -> None: 

851 """ 

852 Close the PyTables file handle 

853 """ 

854 if self._handle is not None: 

855 self._handle.close() 

856 self._handle = None 

857 

858 @property 

859 def is_open(self) -> bool: 

860 """ 

861 Return a boolean indicating whether the file is open. 

862 

863 ``HDFStore`` instances open the underlying PyTables file in their 

864 constructor, but the file can be closed and reopened on the same 

865 instance via :meth:`close` and :meth:`open`. 

866 

867 See Also 

868 -------- 

869 HDFStore.open : Open the underlying file in the specified mode. 

870 HDFStore.close : Close the underlying PyTables file handle. 

871 

872 Examples 

873 -------- 

874 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

875 >>> store.is_open # doctest: +SKIP 

876 True 

877 >>> store.close() # doctest: +SKIP 

878 >>> store.is_open # doctest: +SKIP 

879 False 

880 """ 

881 if self._handle is None: 

882 return False 

883 return bool(self._handle.isopen) 

884 

885 def flush(self, fsync: bool = False) -> None: 

886 """ 

887 Force all buffered modifications to be written to disk. 

888 

889 Useful when sharing access between processes -- call ``flush`` (with 

890 ``fsync=True`` if needed) before releasing a write lock so that 

891 readers see the latest data. 

892 

893 Parameters 

894 ---------- 

895 fsync : bool, default False 

896 Call ``os.fsync()`` on the file handle to force writing to disk. 

897 

898 See Also 

899 -------- 

900 HDFStore.close : Close the underlying PyTables file handle. 

901 

902 Notes 

903 ----- 

904 Without ``fsync=True``, flushing may not guarantee that the OS writes 

905 to disk. With fsync, the operation will block until the OS claims the 

906 file has been written; however, other caching layers may still 

907 interfere. 

908 

909 Examples 

910 -------- 

911 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

912 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

913 >>> store.put("data", df) # doctest: +SKIP 

914 >>> store.flush(fsync=True) # doctest: +SKIP 

915 >>> store.close() # doctest: +SKIP 

916 """ 

917 if self._handle is not None: 

918 self._handle.flush() 

919 if fsync: 

920 with suppress(OSError): 

921 os.fsync(self._handle.fileno()) 

922 

923 def get(self, key: str): 

924 """ 

925 Retrieve pandas object stored in file. 

926 

927 The object is read from the HDF5 file and returned as the 

928 same type that was stored (e.g., DataFrame, Series). 

929 

930 Parameters 

931 ---------- 

932 key : str 

933 Object to retrieve from file. Raises KeyError if not found. 

934 

935 Returns 

936 ------- 

937 object 

938 Same type as object stored in file. 

939 

940 See Also 

941 -------- 

942 HDFStore.get_node : Returns the node with the key. 

943 HDFStore.get_storer : Returns the storer object for a key. 

944 

945 Examples 

946 -------- 

947 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

948 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

949 >>> store.put("data", df) # doctest: +SKIP 

950 >>> store.get("data") # doctest: +SKIP 

951 >>> store.close() # doctest: +SKIP 

952 """ 

953 with patch_pickle(): 

954 # GH#31167 Without this patch, pickle doesn't know how to unpickle 

955 # old DateOffset objects now that they are cdef classes. 

956 group = self.get_node(key) 

957 if group is None: 

958 raise KeyError(f"No object named {key} in the file") 

959 return self._read_group(group) 

960 

961 def select( 

962 self, 

963 key: str, 

964 where=None, 

965 start=None, 

966 stop=None, 

967 columns=None, 

968 iterator: bool = False, 

969 chunksize: int | None = None, 

970 auto_close: bool = False, 

971 ): 

972 """ 

973 Retrieve pandas object stored in file, optionally based on where criteria. 

974 

975 .. warning:: 

976 

977 Pandas uses PyTables for reading and writing HDF5 files, which allows 

978 serializing object-dtype data with pickle when using the "fixed" format. 

979 Loading pickled data received from untrusted sources can be unsafe. 

980 

981 See: https://docs.python.org/3/library/pickle.html for more. 

982 

983 Parameters 

984 ---------- 

985 key : str 

986 Object being retrieved from file. 

987 where : list or None 

988 List of Term (or convertible) objects, optional. 

989 start : int or None 

990 Row number to start selection. 

991 stop : int, default None 

992 Row number to stop selection. 

993 columns : list or None 

994 A list of columns that if not None, will limit the return columns. 

995 iterator : bool or False 

996 Returns an iterator. 

997 chunksize : int or None 

998 Number or rows to include in iteration, return an iterator. 

999 auto_close : bool or False 

1000 Should automatically close the store when finished. 

1001 

1002 Returns 

1003 ------- 

1004 object 

1005 Retrieved object from file. 

1006 

1007 See Also 

1008 -------- 

1009 HDFStore.select_as_coordinates : Returns the selection as an index. 

1010 HDFStore.select_column : Returns a single column from the table. 

1011 HDFStore.select_as_multiple : Retrieves pandas objects from multiple tables. 

1012 

1013 Examples 

1014 -------- 

1015 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

1016 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

1017 >>> store.put("data", df) # doctest: +SKIP 

1018 >>> store.get("data") # doctest: +SKIP 

1019 >>> print(store.keys()) # doctest: +SKIP 

1020 ['/data1', '/data2'] 

1021 >>> store.select("/data1") # doctest: +SKIP 

1022 A B 

1023 0 1 2 

1024 1 3 4 

1025 >>> store.select("/data1", where="columns == A") # doctest: +SKIP 

1026 A 

1027 0 1 

1028 1 3 

1029 >>> store.close() # doctest: +SKIP 

1030 """ 

1031 group = self.get_node(key) 

1032 if group is None: 

1033 raise KeyError(f"No object named {key} in the file") 

1034 

1035 # create the storer and axes 

1036 where = _ensure_term(where, scope_level=1) 

1037 s = self._create_storer(group) 

1038 s.infer_axes() 

1039 

1040 # function to call on iteration 

1041 def func(_start, _stop, _where): 

1042 return s.read(start=_start, stop=_stop, where=_where, columns=columns) 

1043 

1044 # create the iterator 

1045 it = TableIterator( 

1046 self, 

1047 s, 

1048 func, 

1049 where=where, 

1050 nrows=s.nrows, 

1051 start=start, 

1052 stop=stop, 

1053 iterator=iterator, 

1054 chunksize=chunksize, 

1055 auto_close=auto_close, 

1056 ) 

1057 

1058 return it.get_result() 

1059 

1060 def select_as_coordinates( 

1061 self, 

1062 key: str, 

1063 where=None, 

1064 start: int | None = None, 

1065 stop: int | None = None, 

1066 ): 

1067 """ 

1068 Return the selection as an Index. 

1069 

1070 .. warning:: 

1071 

1072 Pandas uses PyTables for reading and writing HDF5 files, which allows 

1073 serializing object-dtype data with pickle when using the "fixed" format. 

1074 Loading pickled data received from untrusted sources can be unsafe. 

1075 

1076 See: https://docs.python.org/3/library/pickle.html for more. 

1077 

1078 Parameters 

1079 ---------- 

1080 key : str 

1081 Object being retrieved from file. 

1082 where : list of Term (or convertible) objects, optional 

1083 Conditions to apply to the selection. ``start`` and ``stop`` are 

1084 applied to the table before ``where``. 

1085 start : int, optional 

1086 Row number to start selection. 

1087 stop : int, optional 

1088 Row number to stop selection. 

1089 

1090 Returns 

1091 ------- 

1092 Index 

1093 Integer positions of the matching rows; can be passed as 

1094 ``where`` to a subsequent ``select``. 

1095 

1096 See Also 

1097 -------- 

1098 HDFStore.select : Retrieve a stored object, optionally filtered by 

1099 ``where``. 

1100 HDFStore.select_as_multiple : Retrieve pandas objects from multiple 

1101 tables. 

1102 

1103 Examples 

1104 -------- 

1105 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

1106 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

1107 >>> store.append( 

1108 ... "data", df, format="table", data_columns=["A"] 

1109 ... ) # doctest: +SKIP 

1110 >>> store.select_as_coordinates("data", "A>1") # doctest: +SKIP 

1111 >>> store.close() # doctest: +SKIP 

1112 """ 

1113 where = _ensure_term(where, scope_level=1) 

1114 tbl = self.get_storer(key) 

1115 if not isinstance(tbl, Table): 

1116 raise TypeError("can only read_coordinates with a table") 

1117 return tbl.read_coordinates(where=where, start=start, stop=stop) 

1118 

1119 def select_column( 

1120 self, 

1121 key: str, 

1122 column: str, 

1123 start: int | None = None, 

1124 stop: int | None = None, 

1125 ): 

1126 """ 

1127 Return a single column from the table. 

1128 

1129 This is generally only useful to select an indexable. 

1130 

1131 .. warning:: 

1132 

1133 Pandas uses PyTables for reading and writing HDF5 files, which allows 

1134 serializing object-dtype data with pickle when using the "fixed" format. 

1135 Loading pickled data received from untrusted sources can be unsafe. 

1136 

1137 See: https://docs.python.org/3/library/pickle.html for more. 

1138 

1139 Parameters 

1140 ---------- 

1141 key : str 

1142 Object being retrieved from file. 

1143 column : str 

1144 The column of interest. 

1145 start : int or None, default None 

1146 Row number to start selection. 

1147 stop : int or None, default None 

1148 Row number to stop selection. 

1149 

1150 Returns 

1151 ------- 

1152 Series 

1153 A ``Series`` of the column's values. 

1154 

1155 Raises 

1156 ------ 

1157 KeyError 

1158 If the column is not found, or ``key`` is not a valid store. 

1159 ValueError 

1160 If the column cannot be extracted individually (not an 

1161 indexable or a data column). 

1162 

1163 See Also 

1164 -------- 

1165 HDFStore.select : Retrieve a stored object, optionally filtered by 

1166 ``where``. 

1167 HDFStore.select_as_coordinates : Return the matching row coordinates 

1168 as an Index. 

1169 

1170 Examples 

1171 -------- 

1172 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

1173 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

1174 >>> store.append( 

1175 ... "data", df, format="table", data_columns=["A"] 

1176 ... ) # doctest: +SKIP 

1177 >>> store.select_column("data", "A") # doctest: +SKIP 

1178 >>> store.close() # doctest: +SKIP 

1179 """ 

1180 tbl = self.get_storer(key) 

1181 if not isinstance(tbl, Table): 

1182 raise TypeError("can only read_column with a table") 

1183 return tbl.read_column(column=column, start=start, stop=stop) 

1184 

1185 def select_as_multiple( 

1186 self, 

1187 keys, 

1188 where=None, 

1189 selector=None, 

1190 columns=None, 

1191 start=None, 

1192 stop=None, 

1193 iterator: bool = False, 

1194 chunksize: int | None = None, 

1195 auto_close: bool = False, 

1196 ): 

1197 """ 

1198 Retrieve pandas objects from multiple tables. 

1199 

1200 .. warning:: 

1201 

1202 Pandas uses PyTables for reading and writing HDF5 files, which allows 

1203 serializing object-dtype data with pickle when using the "fixed" format. 

1204 Loading pickled data received from untrusted sources can be unsafe. 

1205 

1206 See: https://docs.python.org/3/library/pickle.html for more. 

1207 

1208 Parameters 

1209 ---------- 

1210 keys : list of str 

1211 Names of the tables to read. 

1212 where : list, optional 

1213 List of Term (or convertible) objects. 

1214 selector : str, optional 

1215 The table to apply the where criteria to. Defaults to ``keys[0]``. 

1216 columns : list, optional 

1217 Columns to return. 

1218 start : int, optional 

1219 Row number to start selection. Applied to each table before 

1220 ``where`` is evaluated. 

1221 stop : int, optional 

1222 Row number to stop selection. Applied to each table before 

1223 ``where`` is evaluated. 

1224 iterator : bool, default False 

1225 Return an iterator. 

1226 chunksize : int, optional 

1227 Number of rows to include in each iteration; implies 

1228 ``iterator=True``. 

1229 auto_close : bool, default False 

1230 Should automatically close the store when finished. 

1231 

1232 Returns 

1233 ------- 

1234 DataFrame or TableIterator 

1235 Concatenated result from the selected tables. A ``TableIterator`` 

1236 is returned instead when ``iterator=True`` or ``chunksize`` is 

1237 given. 

1238 

1239 Raises 

1240 ------ 

1241 KeyError 

1242 If ``keys`` or ``selector`` is not found, or ``keys`` is empty. 

1243 TypeError 

1244 If ``keys`` is not a list or tuple. 

1245 ValueError 

1246 If the tables do not all have the same number of rows. 

1247 

1248 See Also 

1249 -------- 

1250 HDFStore.append_to_multiple : Append to multiple tables, splitting a 

1251 single object into a dict of column groups. 

1252 HDFStore.select : Retrieve a single stored object. 

1253 

1254 Examples 

1255 -------- 

1256 >>> df1 = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

1257 >>> df2 = pd.DataFrame([[5, 6], [7, 8]], columns=["C", "D"]) 

1258 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

1259 >>> store.append("t1", df1, format="table") # doctest: +SKIP 

1260 >>> store.append("t2", df2, format="table") # doctest: +SKIP 

1261 >>> store.select_as_multiple(["t1", "t2"]) # doctest: +SKIP 

1262 >>> store.close() # doctest: +SKIP 

1263 """ 

1264 # default to single select 

1265 where = _ensure_term(where, scope_level=1) 

1266 if isinstance(keys, (list, tuple)) and len(keys) == 1: 

1267 keys = keys[0] 

1268 if isinstance(keys, str): 

1269 return self.select( 

1270 key=keys, 

1271 where=where, 

1272 columns=columns, 

1273 start=start, 

1274 stop=stop, 

1275 iterator=iterator, 

1276 chunksize=chunksize, 

1277 auto_close=auto_close, 

1278 ) 

1279 

1280 if not isinstance(keys, (list, tuple)): 

1281 raise TypeError("keys must be a list/tuple") 

1282 

1283 if not len(keys): 

1284 raise ValueError("keys must have a non-zero length") 

1285 

1286 if selector is None: 

1287 selector = keys[0] 

1288 

1289 # collect the tables 

1290 tbls = [self.get_storer(k) for k in keys] 

1291 s = self.get_storer(selector) 

1292 

1293 # validate rows 

1294 nrows = None 

1295 for t, k in itertools.chain([(s, selector)], zip(tbls, keys, strict=True)): 

1296 if t is None: 

1297 raise KeyError(f"Invalid table [{k}]") 

1298 if not t.is_table: 

1299 raise TypeError( 

1300 f"object [{t.pathname}] is not a table, and cannot be used in all " 

1301 "select as multiple" 

1302 ) 

1303 

1304 if nrows is None: 

1305 nrows = t.nrows 

1306 elif t.nrows != nrows: 

1307 raise ValueError("all tables must have exactly the same nrows!") 

1308 

1309 # The isinstance checks here are redundant with the check above, 

1310 # but necessary for mypy; see GH#29757 

1311 _tbls = [x for x in tbls if isinstance(x, Table)] 

1312 

1313 # axis is the concentration axes 

1314 axis = {t.non_index_axes[0][0] for t in _tbls}.pop() 

1315 

1316 def func(_start, _stop, _where): 

1317 # retrieve the objs, _where is always passed as a set of 

1318 # coordinates here 

1319 objs = [ 

1320 t.read(where=_where, columns=columns, start=_start, stop=_stop) 

1321 for t in tbls 

1322 ] 

1323 

1324 # concat and return 

1325 return concat(objs, axis=axis, verify_integrity=False)._consolidate() 

1326 

1327 # create the iterator 

1328 it = TableIterator( 

1329 self, 

1330 s, 

1331 func, 

1332 where=where, 

1333 nrows=nrows, 

1334 start=start, 

1335 stop=stop, 

1336 iterator=iterator, 

1337 chunksize=chunksize, 

1338 auto_close=auto_close, 

1339 ) 

1340 

1341 return it.get_result(coordinates=True) 

1342 

1343 def put( 

1344 self, 

1345 key: str, 

1346 value: DataFrame | Series, 

1347 format=None, 

1348 index: bool = True, 

1349 append: bool = False, 

1350 complib=None, 

1351 complevel: int | None = None, 

1352 min_itemsize: int | dict[str, int] | None = None, 

1353 nan_rep=None, 

1354 data_columns: Literal[True] | list[str] | None = None, 

1355 encoding=None, 

1356 errors: str = "strict", 

1357 track_times: bool | lib.NoDefault = lib.no_default, 

1358 dropna: bool | lib.NoDefault = lib.no_default, 

1359 ) -> None: 

1360 """ 

1361 Store object in HDFStore. 

1362 

1363 This method writes a pandas DataFrame or Series into an HDF5 file using 

1364 either the fixed or table format. The `table` format allows additional 

1365 operations like incremental appends and queries but may have performance 

1366 trade-offs. The `fixed` format provides faster read/write operations but 

1367 does not support appends or queries. 

1368 

1369 Parameters 

1370 ---------- 

1371 key : str 

1372 Key of object to store in file. 

1373 value : {Series, DataFrame} 

1374 Value of object to store in file. 

1375 format : 'fixed(f)|table(t)', default is 'fixed' 

1376 Format to use when storing object in HDFStore. Value can be one of: 

1377 

1378 ``'fixed'`` 

1379 Fixed format. Fast writing/reading. Not-appendable, nor searchable. 

1380 ``'table'`` 

1381 Table format. Write as a PyTables Table structure which may perform 

1382 worse but allow more flexible operations like searching / selecting 

1383 subsets of the data. 

1384 index : bool, default True 

1385 Write DataFrame index as a column. 

1386 append : bool, default False 

1387 This will force Table format, append the input data to the existing. 

1388 complib : {'zlib', 'lzo', 'bzip2', 'blosc'}, default None 

1389 Compression library to use, only applied with ``format='table'``. 

1390 None disables compression. See the ``complib`` parameter of 

1391 :class:`HDFStore` for the full list of supported compressors. 

1392 complevel : int, 0-9, default None 

1393 Specifies a compression level for data. 

1394 A value of 0 or None disables compression. 

1395 min_itemsize : int, dict, or None 

1396 Minimum number of bytes reserved for object columns. 

1397 If int, all columns reserve 'min_itemsize' bytes per stored value. 

1398 If dict, specific columns reserve 'min_itemsize' bytes per stored value. 

1399 Strings are stored as encoded bytes. Since some characters require multiple 

1400 bytes, required size may be larger than string length. 

1401 nan_rep : str, optional 

1402 String used on disk to represent missing values in string columns 

1403 (``format="table"`` only). 

1404 By default a sentinel that collides with no value in the column is 

1405 used, so a literal ``"nan"`` round-trips unchanged; when this is 

1406 passed, a value equal to it is read back as a missing value. 

1407 data_columns : list of columns or True, default None 

1408 List of columns to create as data columns, or True to use all columns. 

1409 See `here 

1410 <https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#query-via-data-columns>`__. 

1411 encoding : str, default None 

1412 Provide an encoding for strings. 

1413 errors : str, default 'strict' 

1414 The error handling scheme to use for encoding errors. 

1415 The default is 'strict' meaning that encoding errors raise a 

1416 UnicodeEncodeError. Other possible values are 'ignore', 'replace' and 

1417 'xmlcharrefreplace' as well as any other name registered with 

1418 codecs.register_error that can handle UnicodeEncodeErrors. 

1419 track_times : bool, default True 

1420 Parameter is propagated to 'create_table' method of 'PyTables'. 

1421 If set to False it enables to have the same h5 files (same hashes) 

1422 independent on creation time. 

1423 

1424 .. deprecated:: 3.1.0 

1425 The default value of ``track_times`` will change from ``True`` 

1426 to ``False`` in a future version. Pass ``track_times=False`` 

1427 explicitly to silence this warning and get deterministic 

1428 HDF5 files. 

1429 dropna : bool, default False, optional 

1430 Remove missing values. 

1431 

1432 .. deprecated:: 3.1.0 

1433 The ``dropna`` keyword is deprecated and will be removed in a 

1434 future version. Use :meth:`DataFrame.dropna` before writing 

1435 instead. 

1436 

1437 See Also 

1438 -------- 

1439 HDFStore.info : Prints detailed information on the store. 

1440 HDFStore.get_storer : Returns the storer object for a key. 

1441 

1442 Notes 

1443 ----- 

1444 Writing an empty ``DataFrame`` or ``Series`` with ``format='table'`` 

1445 or ``append=True`` is a no-op: the store is not modified and a 

1446 ``UserWarning`` is emitted. Use ``format='fixed'`` to store an empty 

1447 object. 

1448 

1449 Examples 

1450 -------- 

1451 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

1452 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

1453 >>> store.put("data", df) # doctest: +SKIP 

1454 """ 

1455 if dropna is not lib.no_default: 

1456 warnings.warn( 

1457 "The 'dropna' keyword in HDFStore.put is deprecated and " 

1458 "will be removed in a future version. Use DataFrame.dropna " 

1459 "before writing instead.", 

1460 Pandas4Warning, 

1461 stacklevel=find_stack_level(), 

1462 ) 

1463 else: 

1464 dropna = False 

1465 if format is None: 

1466 format = config["io"]["hdf"]["default_format"] or "fixed" 

1467 format = self._validate_format(format) 

1468 if track_times is lib.no_default: 

1469 warnings.warn( 

1470 "The default value of 'track_times' in HDFStore.put will " 

1471 "change from True to False in a future version. Pass " 

1472 "track_times=False explicitly to silence this warning and " 

1473 "get deterministic HDF5 files.", 

1474 Pandas4Warning, 

1475 stacklevel=find_stack_level(), 

1476 ) 

1477 track_times = True 

1478 self._write_to_group( 

1479 key, 

1480 value, 

1481 format=format, 

1482 index=index, 

1483 append=append, 

1484 complib=complib, 

1485 complevel=complevel, 

1486 min_itemsize=min_itemsize, 

1487 nan_rep=nan_rep, 

1488 data_columns=data_columns, 

1489 encoding=encoding, 

1490 errors=errors, 

1491 track_times=track_times, 

1492 dropna=dropna, 

1493 ) 

1494 

1495 def remove(self, key: str, where=None, start=None, stop=None) -> int | None: 

1496 """ 

1497 Remove pandas object partially by specifying the where condition. 

1498 

1499 If ``where`` is not provided, the entire object stored at ``key`` 

1500 (and any of its child nodes) is removed. When ``where`` is given, 

1501 only matching rows are deleted, which requires the object to be in 

1502 ``table`` format. 

1503 

1504 Parameters 

1505 ---------- 

1506 key : str 

1507 Node to remove or delete rows from. 

1508 where : list of Term (or convertible) objects, optional 

1509 Conditions selecting which rows to remove. 

1510 start : int, optional 

1511 Row number to start selection. 

1512 stop : int, optional 

1513 Row number to stop selection. 

1514 

1515 Returns 

1516 ------- 

1517 int or None 

1518 Number of rows removed (or ``None`` if the object is not a Table). 

1519 

1520 Raises 

1521 ------ 

1522 KeyError 

1523 If ``key`` is not a valid store. 

1524 

1525 See Also 

1526 -------- 

1527 HDFStore.append : Append data to an existing table. 

1528 HDFStore.put : Store an object in the file. 

1529 

1530 Examples 

1531 -------- 

1532 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

1533 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

1534 >>> store.put("data", df) # doctest: +SKIP 

1535 >>> store.remove("data") # doctest: +SKIP 

1536 >>> store.close() # doctest: +SKIP 

1537 """ 

1538 where = _ensure_term(where, scope_level=1) 

1539 try: 

1540 s = self.get_storer(key) 

1541 except KeyError: 

1542 # the key is not a valid store, re-raising KeyError 

1543 raise 

1544 except AssertionError: 

1545 # surface any assertion errors for e.g. debugging 

1546 raise 

1547 except Exception as err: 

1548 # In tests we get here with ClosedFileError, TypeError, and 

1549 # _table_mod.NoSuchNodeError. TODO: Catch only these? 

1550 

1551 if where is not None: 

1552 raise ValueError( 

1553 "trying to remove a node with a non-None where clause!" 

1554 ) from err 

1555 

1556 # we are actually trying to remove a node (with children) 

1557 node = self.get_node(key) 

1558 if node is not None: 

1559 node._f_remove(recursive=True) 

1560 return None 

1561 

1562 # remove the node 

1563 if com.all_none(where, start, stop): 

1564 s.group._f_remove(recursive=True) 

1565 return None 

1566 

1567 # delete from the table 

1568 if not s.is_table: 

1569 raise ValueError("can only remove with where on objects written as tables") 

1570 return s.delete(where=where, start=start, stop=stop) 

1571 

1572 def append( 

1573 self, 

1574 key: str, 

1575 value: DataFrame | Series, 

1576 format=None, 

1577 axes=None, 

1578 index: bool | list[str] = True, 

1579 append: bool = True, 

1580 complib=None, 

1581 complevel: int | None = None, 

1582 columns=None, 

1583 min_itemsize: int | dict[str, int] | None = None, 

1584 nan_rep=None, 

1585 chunksize: int | None = None, 

1586 expectedrows=None, 

1587 dropna: bool | lib.NoDefault | None = lib.no_default, 

1588 data_columns: Literal[True] | list[str] | None = None, 

1589 encoding=None, 

1590 errors: str = "strict", 

1591 ) -> None: 

1592 """ 

1593 Append to Table in file. 

1594 

1595 Node must already exist and be Table format. 

1596 

1597 Parameters 

1598 ---------- 

1599 key : str 

1600 Key of object to append. 

1601 value : {Series, DataFrame} 

1602 Value of object to append. 

1603 format : 'table' is the default 

1604 Format to use when storing object in HDFStore. Value can be one of: 

1605 

1606 ``'table'`` 

1607 Table format. Write as a PyTables Table structure which may perform 

1608 worse but allow more flexible operations like searching / selecting 

1609 subsets of the data. 

1610 axes : list, default None 

1611 Single-element list selecting the axis to store as the table's 

1612 indexed (appendable) axis: ``[0]`` for the index (the default) or 

1613 ``[1]`` for the columns. Ignored when appending to an existing 

1614 table, which keeps the axes it was created with. 

1615 index : bool, default True 

1616 Write DataFrame index as a column. 

1617 append : bool, default True 

1618 Append the input data to the existing. 

1619 complib : {'zlib', 'lzo', 'bzip2', 'blosc'}, default None 

1620 Compression library to use; None disables compression. See the 

1621 ``complib`` parameter of :class:`HDFStore` for the full list of 

1622 supported compressors. 

1623 complevel : int, 0-9, default None 

1624 Specifies a compression level for data. 

1625 A value of 0 or None disables compression. 

1626 columns : default None 

1627 This parameter is currently not accepted, try data_columns. 

1628 min_itemsize : int, dict, or None 

1629 Minimum number of bytes reserved for object columns. 

1630 If int, all columns reserve 'min_itemsize' bytes per stored value. 

1631 If dict, specific columns reserve 'min_itemsize' bytes per stored value. 

1632 Strings are stored as encoded bytes. Since some characters require multiple 

1633 bytes, required size may be larger than string length. 

1634 nan_rep : str, optional 

1635 String used on disk to represent missing values in string columns. 

1636 By default a sentinel that collides with no value in the column is 

1637 used, so a literal ``"nan"`` round-trips unchanged; when this is 

1638 passed, a value equal to it is read back as a missing value. 

1639 Only used when the table is created; ignored on later appends, 

1640 which reuse whatever the table already stores. 

1641 chunksize : int, default 100000 

1642 Number of rows to write in each chunk. 

1643 expectedrows : int 

1644 Expected TOTAL row size of this table. 

1645 dropna : bool, default False, optional 

1646 Do not write an ALL nan row to the store settable 

1647 by the option 'io.hdf.dropna_table'. 

1648 

1649 .. deprecated:: 3.1.0 

1650 The ``dropna`` keyword is deprecated and will be removed in a 

1651 future version. Use :meth:`DataFrame.dropna` before writing 

1652 instead. 

1653 

1654 data_columns : list of columns, or True, default None 

1655 List of columns to create as indexed data columns for on-disk 

1656 queries, or True to use all columns. By default only the axes 

1657 of the object are indexed. See `here 

1658 <https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html#query-via-data-columns>`__. 

1659 encoding : default None 

1660 Provide an encoding for str. 

1661 errors : str, default 'strict' 

1662 The error handling scheme to use for encoding errors. 

1663 The default is 'strict' meaning that encoding errors raise a 

1664 UnicodeEncodeError. Other possible values are 'ignore', 'replace' and 

1665 'xmlcharrefreplace' as well as any other name registered with 

1666 codecs.register_error that can handle UnicodeEncodeErrors. 

1667 

1668 See Also 

1669 -------- 

1670 HDFStore.append_to_multiple : Append to multiple tables. 

1671 

1672 Notes 

1673 ----- 

1674 Does *not* check if data being appended overlaps with existing 

1675 data in the table, so be careful 

1676 

1677 Appending an empty ``DataFrame`` or ``Series`` is a no-op: the store 

1678 is not modified and a ``UserWarning`` is emitted. 

1679 

1680 Examples 

1681 -------- 

1682 >>> df1 = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

1683 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

1684 >>> store.put("data", df1, format="table") # doctest: +SKIP 

1685 >>> df2 = pd.DataFrame([[5, 6], [7, 8]], columns=["A", "B"]) 

1686 >>> store.append("data", df2) # doctest: +SKIP 

1687 >>> store.close() # doctest: +SKIP 

1688 A B 

1689 0 1 2 

1690 1 3 4 

1691 0 5 6 

1692 1 7 8 

1693 """ 

1694 if columns is not None: 

1695 raise TypeError( 

1696 "columns is not a supported keyword in append, try data_columns" 

1697 ) 

1698 

1699 if dropna is not lib.no_default: 

1700 warnings.warn( 

1701 "The 'dropna' keyword in HDFStore.append is deprecated and " 

1702 "will be removed in a future version. Use DataFrame.dropna " 

1703 "before writing instead.", 

1704 Pandas4Warning, 

1705 stacklevel=find_stack_level(), 

1706 ) 

1707 dropna = bool(dropna) if dropna is not None else False 

1708 else: 

1709 dropna = False 

1710 if format is None: 

1711 format = config["io"]["hdf"]["default_format"] or "table" 

1712 format = self._validate_format(format) 

1713 self._write_to_group( 

1714 key, 

1715 value, 

1716 format=format, 

1717 axes=axes, 

1718 index=index, 

1719 append=append, 

1720 complib=complib, 

1721 complevel=complevel, 

1722 min_itemsize=min_itemsize, 

1723 nan_rep=nan_rep, 

1724 chunksize=chunksize, 

1725 expectedrows=expectedrows, 

1726 dropna=dropna, 

1727 data_columns=data_columns, 

1728 encoding=encoding, 

1729 errors=errors, 

1730 ) 

1731 

1732 def append_to_multiple( 

1733 self, 

1734 d: dict, 

1735 value, 

1736 selector, 

1737 data_columns=None, 

1738 axes=None, 

1739 dropna: bool | lib.NoDefault = lib.no_default, 

1740 **kwargs, 

1741 ) -> None: 

1742 """ 

1743 Append to multiple tables. 

1744 

1745 Splits ``value`` column-wise according to ``d`` and appends each 

1746 slice to the corresponding table. The ``selector`` table is the one 

1747 you query against; its columns are made data_columns so they can be 

1748 used in ``where`` clauses. 

1749 

1750 Parameters 

1751 ---------- 

1752 d : dict 

1753 Mapping of table_name to table_columns. ``None`` is acceptable as 

1754 the values for one node (that table will get all the remaining 

1755 columns). 

1756 value : DataFrame or Series 

1757 Pandas object to split across the tables. 

1758 selector : str 

1759 Designates the indexable table; all of its columns will be made 

1760 data_columns unless ``data_columns`` is passed, in which case 

1761 those are used. 

1762 data_columns : list of str or True, optional 

1763 Columns to create as data columns, or ``True`` to use all columns. 

1764 axes : default None 

1765 This parameter is currently not accepted. 

1766 dropna : bool, default False 

1767 If ``True``, drop rows from all tables if any single row in each 

1768 table has all NaN. 

1769 

1770 .. deprecated:: 3.1.0 

1771 The ``dropna`` keyword is deprecated and will be removed in a 

1772 future version. Use :meth:`DataFrame.dropna` before writing 

1773 instead. 

1774 **kwargs 

1775 Additional keyword arguments forwarded to :meth:`HDFStore.append`. 

1776 

1777 See Also 

1778 -------- 

1779 HDFStore.append : Append to a single table. 

1780 HDFStore.select_as_multiple : Read from multiple tables with a 

1781 single ``where``. 

1782 

1783 Examples 

1784 -------- 

1785 >>> df = pd.DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]}) # doctest: +SKIP 

1786 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

1787 >>> store.append_to_multiple( 

1788 ... {"t1": ["A", "B"], "t2": None}, df, selector="t1" 

1789 ... ) # doctest: +SKIP 

1790 >>> store.close() # doctest: +SKIP 

1791 """ 

1792 if axes is not None: 

1793 raise TypeError( 

1794 "axes is currently not accepted as a parameter to append_to_multiple; " 

1795 "you can create the tables independently instead" 

1796 ) 

1797 

1798 if not isinstance(d, dict): 

1799 raise ValueError( 

1800 "append_to_multiple must have a dictionary specified as the " 

1801 "way to split the value" 

1802 ) 

1803 

1804 if selector not in d: 

1805 raise ValueError( 

1806 "append_to_multiple requires a selector that is in passed dict" 

1807 ) 

1808 

1809 # figure out the splitting axis (the non_index_axis) 

1810 axis = next(iter(set(range(value.ndim)) - set(_AXES_MAP[type(value)]))) 

1811 

1812 # figure out how to split the value 

1813 remain_key = None 

1814 remain_values: list = [] 

1815 for k, v in d.items(): 

1816 if v is None: 

1817 if remain_key is not None: 

1818 raise ValueError( 

1819 "append_to_multiple can only have one value in d that is None" 

1820 ) 

1821 remain_key = k 

1822 else: 

1823 remain_values.extend(v) 

1824 if remain_key is not None: 

1825 ordered = value.axes[axis] 

1826 ordd = ordered.difference(Index(remain_values)) 

1827 ordd = sorted(ordered.get_indexer(ordd)) 

1828 d[remain_key] = ordered.take(ordd) 

1829 

1830 # data_columns 

1831 if data_columns is None: 

1832 data_columns = d[selector] 

1833 

1834 # ensure rows are synchronized across the tables 

1835 if dropna is not lib.no_default: 

1836 warnings.warn( 

1837 "The 'dropna' keyword in HDFStore.append_to_multiple is " 

1838 "deprecated and will be removed in a future version. Use " 

1839 "DataFrame.dropna before writing instead.", 

1840 Pandas4Warning, 

1841 stacklevel=find_stack_level(), 

1842 ) 

1843 else: 

1844 dropna = False 

1845 if dropna: 

1846 idxs = (value[cols].dropna(how="all").index for cols in d.values()) 

1847 valid_index = next(idxs) 

1848 for index in idxs: 

1849 valid_index = valid_index.intersection(index) 

1850 value = value.loc[valid_index] 

1851 

1852 min_itemsize = kwargs.pop("min_itemsize", None) 

1853 

1854 # append 

1855 for k, v in d.items(): 

1856 dc = data_columns if k == selector else None 

1857 

1858 # compute the val 

1859 val = value.reindex(v, axis=axis) 

1860 

1861 filtered = ( 

1862 {key: value for (key, value) in min_itemsize.items() if key in v} 

1863 if min_itemsize is not None 

1864 else None 

1865 ) 

1866 self.append(k, val, data_columns=dc, min_itemsize=filtered, **kwargs) 

1867 

1868 def create_table_index( 

1869 self, 

1870 key: str, 

1871 columns=None, 

1872 optlevel: int | None = None, 

1873 kind: str | None = None, 

1874 ) -> None: 

1875 """ 

1876 Create a pytables index on the table. 

1877 

1878 Indexes are automatically created on indexable axes and 

1879 ``data_columns`` during ``append``/``put`` when ``index=True`` 

1880 (the default). This method lets you add or rebuild indexes after 

1881 the fact, which is **highly encouraged** because it greatly speeds 

1882 up ``select`` calls that filter on the indexed dimension. 

1883 

1884 Parameters 

1885 ---------- 

1886 key : str 

1887 Object stored in the file to index. 

1888 columns : None, bool, or listlike[str] 

1889 Indicate which columns to create an index on. 

1890 

1891 * False : Do not create any indexes. 

1892 * True : Create indexes on all columns. 

1893 * None : Create indexes on all columns. 

1894 * listlike : Create indexes on the given columns. 

1895 

1896 optlevel : int or None, default None 

1897 Optimization level, if None, pytables defaults to 6. 

1898 kind : str or None, default None 

1899 Kind of index, if None, pytables defaults to "medium". 

1900 

1901 Raises 

1902 ------ 

1903 TypeError 

1904 If the node is not a table. 

1905 

1906 See Also 

1907 -------- 

1908 HDFStore.append : Append data to an existing table; columns are 

1909 indexed automatically by default. 

1910 HDFStore.select : Filter on indexed columns for fast retrieval. 

1911 

1912 Examples 

1913 -------- 

1914 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

1915 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

1916 >>> store.append("data", df, format="table", index=False) # doctest: +SKIP 

1917 >>> store.create_table_index("data", columns=["A"]) # doctest: +SKIP 

1918 >>> store.close() # doctest: +SKIP 

1919 """ 

1920 # version requirements 

1921 _tables() 

1922 s = self.get_storer(key) 

1923 if s is None: 

1924 return 

1925 

1926 if not isinstance(s, Table): 

1927 raise TypeError("cannot create table index on a Fixed format store") 

1928 s.create_index(columns=columns, optlevel=optlevel, kind=kind) 

1929 

1930 def groups(self) -> list: 

1931 """ 

1932 Return a list of all the top-level nodes. 

1933 

1934 Each node returned is not a pandas storage object. 

1935 

1936 Returns 

1937 ------- 

1938 list 

1939 List of objects. 

1940 

1941 See Also 

1942 -------- 

1943 HDFStore.get_node : Returns the node with the key. 

1944 

1945 Examples 

1946 -------- 

1947 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

1948 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

1949 >>> store.put("data", df) # doctest: +SKIP 

1950 >>> print(store.groups()) # doctest: +SKIP 

1951 >>> store.close() # doctest: +SKIP 

1952 [/data (Group) '' 

1953 children := ['axis0' (Array), 'axis1' (Array), 'block0_values' (Array), 

1954 'block0_items' (Array)]] 

1955 """ 

1956 _tables() 

1957 self._check_if_open() 

1958 assert self._handle is not None # for mypy 

1959 assert _table_mod is not None # for mypy 

1960 return [ 

1961 g 

1962 for g in self._handle.walk_groups() 

1963 if ( 

1964 not isinstance(g, _table_mod.link.Link) 

1965 and ( 

1966 getattr(g._v_attrs, "pandas_type", None) 

1967 or getattr(g, "table", None) 

1968 or (isinstance(g, _table_mod.table.Table) and g._v_name != "table") 

1969 ) 

1970 ) 

1971 ] 

1972 

1973 def walk(self, where: str = "/") -> Iterator[tuple[str, list[str], list[str]]]: 

1974 """ 

1975 Walk the pytables group hierarchy for pandas objects. 

1976 

1977 This generator will yield the group path, subgroups and pandas object 

1978 names for each group. 

1979 

1980 Any non-pandas PyTables objects that are not a group will be ignored. 

1981 

1982 The `where` group itself is listed first (preorder), then each of its 

1983 child groups (following an alphanumerical order) is also traversed, 

1984 following the same procedure. 

1985 

1986 Parameters 

1987 ---------- 

1988 where : str, default "/" 

1989 Group where to start walking. 

1990 

1991 Yields 

1992 ------ 

1993 path : str 

1994 Full path to a group (without trailing '/'). 

1995 groups : list 

1996 Names (strings) of the groups contained in `path`. 

1997 leaves : list 

1998 Names (strings) of the pandas objects contained in `path`. 

1999 

2000 See Also 

2001 -------- 

2002 HDFStore.info : Prints detailed information on the store. 

2003 

2004 Examples 

2005 -------- 

2006 >>> df1 = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

2007 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

2008 >>> store.put("data", df1, format="table") # doctest: +SKIP 

2009 >>> df2 = pd.DataFrame([[5, 6], [7, 8]], columns=["A", "B"]) 

2010 >>> store.append("data", df2) # doctest: +SKIP 

2011 >>> store.close() # doctest: +SKIP 

2012 >>> for group in store.walk(): # doctest: +SKIP 

2013 ... print(group) # doctest: +SKIP 

2014 >>> store.close() # doctest: +SKIP 

2015 """ 

2016 _tables() 

2017 self._check_if_open() 

2018 assert self._handle is not None # for mypy 

2019 assert _table_mod is not None # for mypy 

2020 

2021 for g in self._handle.walk_groups(where): 

2022 if getattr(g._v_attrs, "pandas_type", None) is not None: 

2023 continue 

2024 

2025 groups = [] 

2026 leaves = [] 

2027 for child in g._v_children.values(): 

2028 pandas_type = getattr(child._v_attrs, "pandas_type", None) 

2029 if pandas_type is None: 

2030 if isinstance(child, _table_mod.group.Group): 

2031 groups.append(child._v_name) 

2032 else: 

2033 leaves.append(child._v_name) 

2034 

2035 yield (g._v_pathname.rstrip("/"), groups, leaves) 

2036 

2037 def get_node(self, key: str) -> Node | None: 

2038 """return the node with the key or None if it does not exist""" 

2039 self._check_if_open() 

2040 if not key.startswith("/"): 

2041 key = "/" + key 

2042 

2043 assert self._handle is not None 

2044 assert _table_mod is not None # for mypy 

2045 try: 

2046 node = self._handle.get_node(self.root, key) 

2047 except _table_mod.exceptions.NoSuchNodeError: 

2048 return None 

2049 

2050 assert isinstance(node, _table_mod.Node), type(node) 

2051 return node 

2052 

2053 def get_storer(self, key: str) -> GenericFixed | Table: 

2054 """ 

2055 Return the storer object for a key. 

2056 

2057 The storer is the low-level wrapper around the stored pandas object. 

2058 It exposes implementation details such as ``nrows`` (the row count 

2059 on disk) and ``table`` (the underlying PyTables ``Table``), which 

2060 can be useful for inspecting a store without loading its data. 

2061 

2062 Parameters 

2063 ---------- 

2064 key : str 

2065 Object stored in the file. 

2066 

2067 Returns 

2068 ------- 

2069 GenericFixed or Table 

2070 The storer wrapping the stored object. ``Table`` instances expose 

2071 attributes such as ``nrows`` and ``table``. 

2072 

2073 Raises 

2074 ------ 

2075 KeyError 

2076 If ``key`` is not in the file. 

2077 

2078 See Also 

2079 -------- 

2080 HDFStore.get : Read the stored object back into pandas. 

2081 HDFStore.info : Print a summary of the store's contents. 

2082 

2083 Examples 

2084 -------- 

2085 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

2086 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

2087 >>> store.append("data", df, format="table") # doctest: +SKIP 

2088 >>> store.get_storer("data").nrows # doctest: +SKIP 

2089 >>> store.close() # doctest: +SKIP 

2090 """ 

2091 group = self.get_node(key) 

2092 if group is None: 

2093 raise KeyError(f"No object named {key} in the file") 

2094 

2095 s = self._create_storer(group) 

2096 s.infer_axes() 

2097 return s 

2098 

2099 def copy( 

2100 self, 

2101 file, 

2102 mode: str = "w", 

2103 propindexes: bool = True, 

2104 keys=None, 

2105 complib=None, 

2106 complevel: int | None = None, 

2107 fletcher32: bool = False, 

2108 overwrite: bool = True, 

2109 ) -> HDFStore: 

2110 """ 

2111 Copy the existing store to a new file, updating in place. 

2112 

2113 Each object stored in the current ``HDFStore`` is read and re-written 

2114 to the destination file, which can be useful for repacking (to 

2115 reclaim space after deletes), changing compression options, or 

2116 cloning a subset of keys. 

2117 

2118 Parameters 

2119 ---------- 

2120 file : str or path-like 

2121 Destination file to write to. 

2122 mode : str, default 'w' 

2123 File mode for the destination, see :class:`HDFStore`. 

2124 propindexes : bool, default True 

2125 Restore indexes in copied file. 

2126 keys : list, optional 

2127 List of keys to include in the copy (defaults to all). 

2128 complib : str, optional 

2129 Compression library, see :class:`HDFStore`. 

2130 complevel : int, optional 

2131 Compression level, see :class:`HDFStore`. 

2132 fletcher32 : bool, default False 

2133 Whether to use the Fletcher32 checksum, see :class:`HDFStore`. 

2134 overwrite : bool, default True 

2135 Whether to overwrite (remove and replace) existing nodes in the 

2136 new store. 

2137 

2138 Returns 

2139 ------- 

2140 HDFStore 

2141 Open file handle of the new store. 

2142 

2143 See Also 

2144 -------- 

2145 HDFStore.put : Write an object to the store. 

2146 HDFStore.append : Append to an existing table. 

2147 

2148 Examples 

2149 -------- 

2150 >>> df = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

2151 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

2152 >>> store.put("data", df) # doctest: +SKIP 

2153 >>> new_store = store.copy("store_copy.h5") # doctest: +SKIP 

2154 >>> new_store.close() # doctest: +SKIP 

2155 >>> store.close() # doctest: +SKIP 

2156 """ 

2157 new_store = HDFStore( 

2158 file, mode=mode, complib=complib, complevel=complevel, fletcher32=fletcher32 

2159 ) 

2160 if keys is None: 

2161 keys = list(self.keys()) 

2162 if not isinstance(keys, (tuple, list)): 

2163 keys = [keys] 

2164 for k in keys: 

2165 s = self.get_storer(k) 

2166 if s is not None: 

2167 if k in new_store: 

2168 if overwrite: 

2169 new_store.remove(k) 

2170 

2171 data = self.select(k) 

2172 if isinstance(s, Table): 

2173 index: bool | list[str] = False 

2174 if propindexes: 

2175 index = [a.name for a in s.axes if a.is_indexed] 

2176 new_store.append( 

2177 k, 

2178 data, 

2179 index=index, 

2180 data_columns=getattr(s, "data_columns", None), 

2181 encoding=s.encoding, 

2182 ) 

2183 else: 

2184 new_store.put(k, data, encoding=s.encoding, track_times=False) 

2185 

2186 return new_store 

2187 

2188 def info(self) -> str: 

2189 """ 

2190 Print detailed information on the store. 

2191 

2192 The information includes the file path, class name, and a listing 

2193 of all stored object keys with their types and shapes. 

2194 

2195 Returns 

2196 ------- 

2197 str 

2198 A String containing the python pandas class name, filepath to the HDF5 

2199 file and all the object keys along with their respective dataframe shapes. 

2200 

2201 See Also 

2202 -------- 

2203 HDFStore.get_storer : Returns the storer object for a key. 

2204 

2205 Examples 

2206 -------- 

2207 >>> df1 = pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) 

2208 >>> df2 = pd.DataFrame([[5, 6], [7, 8]], columns=["C", "D"]) 

2209 >>> store = pd.HDFStore("store.h5", "w") # doctest: +SKIP 

2210 >>> store.put("data1", df1) # doctest: +SKIP 

2211 >>> store.put("data2", df2) # doctest: +SKIP 

2212 >>> print(store.info()) # doctest: +SKIP 

2213 >>> store.close() # doctest: +SKIP 

2214 <class 'pandas.io.pytables.HDFStore'> 

2215 File path: store.h5 

2216 /data1 frame (shape->[2,2]) 

2217 /data2 frame (shape->[2,2]) 

2218 """ 

2219 path = pprint_thing(self._path) 

2220 output = f"{type(self)}\nFile path: {path}\n" 

2221 

2222 if self.is_open: 

2223 lkeys = sorted(self.keys()) 

2224 if lkeys: 

2225 keys = [] 

2226 values = [] 

2227 

2228 for k in lkeys: 

2229 try: 

2230 s = self.get_storer(k) 

2231 if s is not None: 

2232 keys.append(pprint_thing(s.pathname or k)) 

2233 values.append(pprint_thing(s or "invalid_HDFStore node")) 

2234 except AssertionError: 

2235 # surface any assertion errors for e.g. debugging 

2236 raise 

2237 except Exception as detail: 

2238 keys.append(k) 

2239 dstr = pprint_thing(detail) 

2240 values.append(f"[invalid_HDFStore node: {dstr}]") 

2241 

2242 output += adjoin(12, keys, values) 

2243 else: 

2244 output += "Empty" 

2245 else: 

2246 output += "File is CLOSED" 

2247 

2248 return output 

2249 

2250 # ------------------------------------------------------------------------ 

2251 # private methods 

2252 

2253 def _check_if_open(self) -> None: 

2254 if not self.is_open: 

2255 raise ClosedFileError(f"{self._path} file is not open!") 

2256 

2257 def _validate_format(self, format: str) -> str: 

2258 """validate / deprecate formats""" 

2259 # validate 

2260 try: 

2261 format = _FORMAT_MAP[format.lower()] 

2262 except KeyError as err: 

2263 raise TypeError(f"invalid HDFStore format specified [{format}]") from err 

2264 

2265 return format 

2266 

2267 def _create_storer( 

2268 self, 

2269 group, 

2270 format=None, 

2271 value: DataFrame | Series | None = None, 

2272 encoding: str = "UTF-8", 

2273 errors: str = "strict", 

2274 ) -> GenericFixed | Table: 

2275 """return a suitable class to operate""" 

2276 cls: type[GenericFixed | Table] 

2277 

2278 if value is not None and not isinstance(value, (Series, DataFrame)): 

2279 raise TypeError("value must be None, Series, or DataFrame") 

2280 

2281 pt = getattr(group._v_attrs, "pandas_type", None) 

2282 tt = getattr(group._v_attrs, "table_type", None) 

2283 

2284 # infer the pt from the passed value 

2285 if pt is None: 

2286 if value is None: 

2287 _tables() 

2288 assert _table_mod is not None # for mypy 

2289 if getattr(group, "table", None) or isinstance( 

2290 group, _table_mod.table.Table 

2291 ): 

2292 pt = "frame_table" 

2293 tt = "generic_table" 

2294 else: 

2295 raise TypeError( 

2296 "cannot create a storer if the object is not existing " 

2297 "nor a value are passed" 

2298 ) 

2299 else: 

2300 if isinstance(value, Series): 

2301 pt = "series" 

2302 else: 

2303 pt = "frame" 

2304 

2305 # we are actually a table 

2306 if format == "table": 

2307 pt += "_table" 

2308 

2309 # a storer node 

2310 if "table" not in pt: 

2311 _STORER_MAP = {"series": SeriesFixed, "frame": FrameFixed} 

2312 try: 

2313 cls = _STORER_MAP[pt] 

2314 except KeyError as err: 

2315 raise TypeError( 

2316 f"cannot properly create the storer for: [_STORER_MAP] [group->" 

2317 f"{group},value->{type(value)},format->{format}" 

2318 ) from err 

2319 return cls(self, group, encoding=encoding, errors=errors) 

2320 

2321 # existing node (and must be a table) 

2322 if tt is None: 

2323 # if we are a writer, determine the tt 

2324 if value is not None: 

2325 if pt == "series_table": 

2326 index = getattr(value, "index", None) 

2327 if index is not None: 

2328 if index.nlevels == 1: 

2329 tt = "appendable_series" 

2330 elif index.nlevels > 1: 

2331 tt = "appendable_multiseries" 

2332 elif pt == "frame_table": 

2333 index = getattr(value, "index", None) 

2334 if index is not None: 

2335 if index.nlevels == 1: 

2336 tt = "appendable_frame" 

2337 elif index.nlevels > 1: 

2338 tt = "appendable_multiframe" 

2339 

2340 _TABLE_MAP = { 

2341 "generic_table": GenericTable, 

2342 "appendable_series": AppendableSeriesTable, 

2343 "appendable_multiseries": AppendableMultiSeriesTable, 

2344 "appendable_frame": AppendableFrameTable, 

2345 "appendable_multiframe": AppendableMultiFrameTable, 

2346 "worm": WORMTable, 

2347 } 

2348 try: 

2349 cls = _TABLE_MAP[tt] # type: ignore[index] 

2350 except KeyError as err: 

2351 raise TypeError( 

2352 f"cannot properly create the storer for: [_TABLE_MAP] [group->" 

2353 f"{group},value->{type(value)},format->{format}" 

2354 ) from err 

2355 

2356 return cls(self, group, encoding=encoding, errors=errors) 

2357 

2358 def _write_to_group( 

2359 self, 

2360 key: str, 

2361 value: DataFrame | Series, 

2362 format, 

2363 axes=None, 

2364 index: bool | list[str] = True, 

2365 append: bool = False, 

2366 complib=None, 

2367 complevel: int | None = None, 

2368 fletcher32=None, 

2369 min_itemsize: int | dict[str, int] | None = None, 

2370 chunksize: int | None = None, 

2371 expectedrows=None, 

2372 dropna: bool = False, 

2373 nan_rep=None, 

2374 data_columns=None, 

2375 encoding=None, 

2376 errors: str = "strict", 

2377 track_times: bool = True, 

2378 ) -> None: 

2379 # we don't want to store a table node at all if our object is 0-len 

2380 # as there are no dtypes 

2381 if getattr(value, "empty", None) and (format == "table" or append): 

2382 warnings.warn( 

2383 "Writing an empty DataFrame or Series with format='table' " 

2384 "or append=True is a no-op; the HDFStore is not modified.", 

2385 UserWarning, 

2386 stacklevel=find_stack_level(), 

2387 ) 

2388 return 

2389 

2390 group = self._identify_group(key, append) 

2391 

2392 s = self._create_storer(group, format, value, encoding=encoding, errors=errors) 

2393 if append: 

2394 # raise if we are trying to append to a Fixed format, 

2395 # or a table that exists (and we are putting) 

2396 if not s.is_table or (s.is_table and format == "fixed" and s.is_exists): 

2397 raise ValueError( 

2398 f"Can only append to Tables; the write for key {key!r} " 

2399 "uses the 'fixed' format. Pass format='table'; if a " 

2400 "'fixed'-format object already exists at this key, remove " 

2401 "or overwrite it first." 

2402 ) 

2403 if not s.is_exists: 

2404 s.set_object_info() 

2405 else: 

2406 s.set_object_info() 

2407 

2408 if not s.is_table and complib: 

2409 raise ValueError("Compression not supported on Fixed format stores") 

2410 

2411 # write the object 

2412 s.write( 

2413 obj=value, 

2414 axes=axes, 

2415 append=append, 

2416 complib=complib, 

2417 complevel=complevel, 

2418 fletcher32=fletcher32, 

2419 min_itemsize=min_itemsize, 

2420 chunksize=chunksize, 

2421 expectedrows=expectedrows, 

2422 dropna=dropna, 

2423 nan_rep=nan_rep, 

2424 data_columns=data_columns, 

2425 track_times=track_times, 

2426 ) 

2427 

2428 if isinstance(s, Table) and index: 

2429 s.create_index(columns=index) 

2430 

2431 def _read_group(self, group: Node): 

2432 s = self._create_storer(group) 

2433 s.infer_axes() 

2434 return s.read() 

2435 

2436 def _identify_group(self, key: str, append: bool) -> Node: 

2437 """Identify HDF5 group based on key, delete/create group if needed.""" 

2438 group = self.get_node(key) 

2439 

2440 # we make this assertion for mypy; the get_node call will already 

2441 # have raised if this is incorrect 

2442 assert self._handle is not None 

2443 

2444 # remove the node if we are not appending 

2445 if group is not None and not append: 

2446 assert _table_mod is not None # for mypy 

2447 children = list(group._v_children.values()) 

2448 # a "meta" subgroup of a stored object holds its metadata (e.g. 

2449 # categories for table-format categoricals), not a nested key 

2450 is_stored_object = getattr(group._v_attrs, "pandas_type", None) is not None 

2451 nested_keys = [ 

2452 child 

2453 for child in children 

2454 if isinstance(child, _table_mod.group.Group) 

2455 and not (is_stored_object and child._v_name == "meta") 

2456 ] 

2457 if nested_keys: 

2458 # GH#17267: the group has child keys nested underneath it, so a 

2459 # recursive removal would silently delete them. Remove only the 

2460 # nodes of the object stored at this key and reset its 

2461 # attributes, leaving the nested keys intact. 

2462 for child in children: 

2463 if child not in nested_keys: 

2464 self._handle.remove_node(child, recursive=True) 

2465 for attr_name in group._v_attrs._f_list("user"): 

2466 delattr(group._v_attrs, attr_name) 

2467 else: 

2468 self._handle.remove_node(group, recursive=True) 

2469 group = None 

2470 

2471 if group is None: 

2472 group = self._create_nodes_and_group(key) 

2473 

2474 return group 

2475 

2476 def _create_nodes_and_group(self, key: str) -> Node: 

2477 """Create nodes from key and return group name.""" 

2478 # assertion for mypy 

2479 assert self._handle is not None 

2480 

2481 paths = key.split("/") 

2482 # recursively create the groups 

2483 path = "/" 

2484 for p in paths: 

2485 if not len(p): 

2486 continue 

2487 new_path = path 

2488 if not path.endswith("/"): 

2489 new_path += "/" 

2490 new_path += p 

2491 group = self.get_node(new_path) 

2492 if group is None: 

2493 group = self._handle.create_group(path, p) 

2494 path = new_path 

2495 return group 

2496 

2497 

2498class TableIterator: 

2499 """ 

2500 Define the iteration interface on a table 

2501 

2502 Parameters 

2503 ---------- 

2504 store : HDFStore 

2505 s : the referred storer 

2506 func : the function to execute the query 

2507 where : the where of the query 

2508 nrows : the rows to iterate on 

2509 start : the passed start value (default is None) 

2510 stop : the passed stop value (default is None) 

2511 iterator : bool, default False 

2512 Whether to use the default iterator. 

2513 chunksize : the passed chunking value (default is 100000) 

2514 auto_close : bool, default False 

2515 Whether to automatically close the store at the end of iteration. 

2516 """ 

2517 

2518 chunksize: int | None 

2519 store: HDFStore 

2520 s: GenericFixed | Table 

2521 

2522 def __init__( 

2523 self, 

2524 store: HDFStore, 

2525 s: GenericFixed | Table, 

2526 func, 

2527 where, 

2528 nrows, 

2529 start=None, 

2530 stop=None, 

2531 iterator: bool = False, 

2532 chunksize: int | None = None, 

2533 auto_close: bool = False, 

2534 ) -> None: 

2535 self.store = store 

2536 self.s = s 

2537 self.func = func 

2538 self.where = where 

2539 

2540 # set start/stop if they are not set if we are a table 

2541 if self.s.is_table: 

2542 if nrows is None: 

2543 nrows = 0 

2544 start, stop = _resolve_row_window(start, stop, nrows) 

2545 

2546 self.nrows = nrows 

2547 self.start = start 

2548 self.stop = stop 

2549 

2550 self.coordinates = None 

2551 self._called_get_result = False 

2552 if iterator or chunksize is not None: 

2553 if chunksize is None: 

2554 chunksize = 100000 

2555 self.chunksize = int(chunksize) 

2556 else: 

2557 self.chunksize = None 

2558 

2559 self.auto_close = auto_close 

2560 

2561 def __iter__(self) -> Iterator: 

2562 # iterate 

2563 if not self._called_get_result: 

2564 raise ValueError("Cannot iterate until get_result is called.") 

2565 current = self.start 

2566 while current < self.stop: 

2567 stop = min(current + self.chunksize, self.stop) 

2568 if self.coordinates is None: 

2569 # no `where` filter: iterate by (start, stop) directly so we 

2570 # don't materialize an np.arange(0, nrows) coordinate array, 

2571 # which can exhaust memory on very large tables (GH#15937) 

2572 value = self.func(current, stop, None) 

2573 else: 

2574 value = self.func(None, None, self.coordinates[current:stop]) 

2575 current = stop 

2576 if value is None or not len(value): 

2577 continue 

2578 

2579 yield value 

2580 

2581 self.close() 

2582 

2583 def close(self) -> None: 

2584 if self.auto_close: 

2585 self.store.close() 

2586 

2587 def get_result(self, coordinates: bool = False): 

2588 # return the actual iterator 

2589 self._called_get_result = True 

2590 if self.chunksize is not None: 

2591 if not isinstance(self.s, Table): 

2592 raise TypeError("can only use an iterator or chunksize on a table") 

2593 

2594 if self.where is not None: 

2595 # read_coordinates raises a clear NotImplementedError if the 

2596 # where is a column projection (e.g. "columns=['A']") rather 

2597 # than a row filter (GH#12953). 

2598 self.coordinates = self.s.read_coordinates(where=self.where) 

2599 

2600 return self 

2601 

2602 # if specified read via coordinates (necessary for multiple selections 

2603 # so each table reads the same row set). Skip when self.where is None 

2604 # since every row would be selected anyway, and a coordinate-based read 

2605 # is much slower than a sequential read (GH#26771). 

2606 if coordinates and self.where is not None: 

2607 if not isinstance(self.s, Table): 

2608 raise TypeError("can only read_coordinates on a table") 

2609 where = self.s.read_coordinates( 

2610 where=self.where, start=self.start, stop=self.stop 

2611 ) 

2612 else: 

2613 where = self.where 

2614 

2615 # directly return the result 

2616 results = self.func(self.start, self.stop, where) 

2617 self.close() 

2618 return results 

2619 

2620 

2621class IndexCol: 

2622 """ 

2623 an index column description class 

2624 

2625 Parameters 

2626 ---------- 

2627 axis : axis which I reference 

2628 values : the ndarray like converted values 

2629 kind : a string description of this type 

2630 typ : the pytables type 

2631 pos : the position in the pytables 

2632 

2633 """ 

2634 

2635 is_an_indexable: bool = True 

2636 is_data_indexable: bool = True 

2637 # GH#9604: set on a DataCol whose missing values use the sentinel in 

2638 # ``nan_rep`` rather than a table-wide one (an old file, or a table written 

2639 # with an explicit nan_rep). 

2640 uses_col_nan_rep: bool = False 

2641 _info_fields = ["freq", "tz", "index_name", "ordered"] 

2642 

2643 def __init__( 

2644 self, 

2645 name: str, 

2646 values=None, 

2647 kind=None, 

2648 typ=None, 

2649 cname: str | None = None, 

2650 axis=None, 

2651 pos=None, 

2652 freq=None, 

2653 tz=None, 

2654 index_name=None, 

2655 ordered=None, 

2656 table=None, 

2657 meta=None, 

2658 metadata=None, 

2659 nan_rep=None, 

2660 ) -> None: 

2661 if not isinstance(name, str): 

2662 raise ValueError("`name` must be a str.") 

2663 

2664 self.values = values 

2665 self.kind = kind 

2666 self.typ = typ 

2667 self.name = name 

2668 self.cname = cname or name 

2669 self.axis = axis 

2670 self.pos = pos 

2671 self.freq = freq 

2672 self.tz = tz 

2673 self.index_name = index_name 

2674 self.ordered = ordered 

2675 self.table = table 

2676 self.meta = meta 

2677 self.metadata = metadata 

2678 # GH#9604: the sentinel used on write to encode this column's missing 

2679 # values (None when it had none). 

2680 self.nan_rep = nan_rep 

2681 

2682 if pos is not None: 

2683 self.set_pos(pos) 

2684 

2685 # These are ensured as long as the passed arguments match the 

2686 # constructor annotations. 

2687 assert isinstance(self.name, str) 

2688 assert isinstance(self.cname, str) 

2689 

2690 @property 

2691 def itemsize(self) -> int: 

2692 # Assumes self.typ has already been initialized 

2693 return self.typ.itemsize 

2694 

2695 @property 

2696 def kind_attr(self) -> str: 

2697 return f"{self.name}_kind" 

2698 

2699 @property 

2700 def meta_attr(self) -> str: 

2701 return f"{self.name}_meta" 

2702 

2703 @property 

2704 def nan_rep_attr(self) -> str: 

2705 return f"{self.name}_nan_rep" 

2706 

2707 def set_pos(self, pos: int) -> None: 

2708 """set the position of this column in the Table""" 

2709 self.pos = pos 

2710 if pos is not None and self.typ is not None: 

2711 self.typ._v_pos = pos 

2712 

2713 def __repr__(self) -> str: 

2714 temp = tuple( 

2715 map(pprint_thing, (self.name, self.cname, self.axis, self.pos, self.kind)) 

2716 ) 

2717 return ",".join( 

2718 [ 

2719 f"{key}->{value}" 

2720 for key, value in zip( 

2721 ["name", "cname", "axis", "pos", "kind"], temp, strict=True 

2722 ) 

2723 ] 

2724 ) 

2725 

2726 def __eq__(self, other: object) -> bool: 

2727 """compare 2 col items""" 

2728 return all( 

2729 getattr(self, a, None) == getattr(other, a, None) 

2730 for a in ["name", "cname", "axis", "pos"] 

2731 ) 

2732 

2733 def __ne__(self, other) -> bool: 

2734 return not self.__eq__(other) 

2735 

2736 @property 

2737 def is_indexed(self) -> bool: 

2738 """return whether I am an indexed column""" 

2739 if not hasattr(self.table, "cols"): 

2740 # e.g. if infer hasn't been called yet, self.table will be None. 

2741 return False 

2742 return getattr(self.table.cols, self.cname).is_indexed 

2743 

2744 def convert( 

2745 self, values: np.ndarray, nan_rep, encoding: str, errors: str 

2746 ) -> tuple[np.ndarray, np.ndarray] | tuple[Index, Index]: 

2747 """ 

2748 Convert the data from this selection to the appropriate pandas type. 

2749 """ 

2750 assert isinstance(values, np.ndarray), type(values) 

2751 

2752 # values is a recarray 

2753 if values.dtype.fields is not None: 

2754 # Copy, otherwise values will be a view 

2755 # preventing the original recarry from being free'ed 

2756 values = values[self.cname].copy() 

2757 

2758 if self.meta == "category": 

2759 # GH#33909, GH#16118: reconstruct a CategoricalIndex from the 

2760 # stored integer codes and the categories saved as metadata. 

2761 categories = self.metadata 

2762 codes = values.ravel() 

2763 

2764 # Mirror DataCol.convert's NaN-category handling so a 

2765 # CategoricalIndex round-trips through format="table". GH#65576 

2766 if categories is None: 

2767 # Zero-category (all-NaN) case: PyTables cannot store an 

2768 # empty metadata array, so it round-trips as None. 

2769 categories = Index([], dtype=np.float64) 

2770 else: 

2771 mask = isna(categories) 

2772 if mask.any(): 

2773 # A category decodes to NaN when it equaled the nan_rep 

2774 # string on write; drop NaN categories and remap the 

2775 # codes so from_codes does not reject a null category. 

2776 remap = np.full(len(categories), -1, dtype=codes.dtype) 

2777 remap[~mask] = np.arange((~mask).sum(), dtype=codes.dtype) 

2778 categories = categories[~mask] 

2779 codes = np.where(codes < 0, codes, remap[codes]) 

2780 

2781 cat = Categorical.from_codes( 

2782 codes, 

2783 categories=categories, 

2784 ordered=bool(self.ordered), 

2785 validate=False, 

2786 ) 

2787 cat_index = CategoricalIndex._simple_new(cat, name=self.index_name) 

2788 return cat_index, cat_index 

2789 

2790 val_kind = self.kind 

2791 # GH#9604: self.nan_rep (set from the persisted per-index sentinel) lets 

2792 # a string Index restore missing values while leaving a literal "nan". 

2793 values = _maybe_convert(values, val_kind, encoding, errors, self.nan_rep) 

2794 kwargs = {} 

2795 kwargs["name"] = self.index_name 

2796 

2797 if val_kind == "string" and using_string_dtype(): 

2798 # GH#9604: an all-missing string Index -- or an all-missing 

2799 # slice/chunk of one -- cannot be inferred back to str, which 

2800 # would make the index dtype depend on which rows were read. 

2801 kwargs["dtype"] = "str" 

2802 

2803 if self.freq is not None: 

2804 kwargs["freq"] = self.freq 

2805 

2806 factory: type[Index | DatetimeIndex | TimedeltaIndex] = Index 

2807 if lib.is_np_dtype(values.dtype, "M") or isinstance( 

2808 values.dtype, DatetimeTZDtype 

2809 ): 

2810 factory = DatetimeIndex 

2811 elif val_kind.startswith("timedelta64"): 

2812 # GH#21466 timedelta values are stored as i8; restore the original 

2813 # m8[unit] view so we round-trip to TimedeltaIndex (and not to 

2814 # PeriodIndex/Index via the i8 branches below). 

2815 if val_kind == "timedelta64": 

2816 # legacy file: written before we stored timedelta64 resolution 

2817 values = values.view("m8[ns]") 

2818 else: 

2819 values = values.view(val_kind) 

2820 factory = TimedeltaIndex 

2821 elif values.dtype == "i8" and "freq" in kwargs: 

2822 # PeriodIndex data is stored as i8 

2823 # error: Incompatible types in assignment (expression has type 

2824 # "Callable[[Any, KwArg(Any)], PeriodIndex]", variable has type 

2825 # "Union[Type[Index], Type[DatetimeIndex]]") 

2826 factory = lambda x, **kwds: PeriodIndex.from_ordinals( # type: ignore[assignment] 

2827 x, freq=kwds.get("freq", None) 

2828 )._rename(kwds["name"]) 

2829 

2830 # making an Index instance could throw a number of different errors 

2831 try: 

2832 new_pd_index = factory(values, **kwargs) 

2833 except UnicodeEncodeError as err: 

2834 if ( 

2835 errors == "surrogatepass" 

2836 and using_string_dtype() 

2837 and str(err).endswith("surrogates not allowed") 

2838 and HAS_PYARROW 

2839 ): 

2840 kwargs["dtype"] = StringDtype(storage="python", na_value=np.nan) 

2841 new_pd_index = factory(values, **kwargs) 

2842 else: 

2843 raise 

2844 except ValueError: 

2845 # if the output freq is different that what we recorded, 

2846 # it should be None (see also 'doc example part 2') 

2847 if "freq" in kwargs: 

2848 kwargs["freq"] = None 

2849 new_pd_index = factory(values, **kwargs) 

2850 

2851 final_pd_index: Index 

2852 if self.tz is not None and isinstance(new_pd_index, DatetimeIndex): 

2853 final_pd_index = new_pd_index.tz_localize("UTC").tz_convert(self.tz) 

2854 else: 

2855 final_pd_index = new_pd_index 

2856 return final_pd_index, final_pd_index 

2857 

2858 def take_data(self): 

2859 """return the values""" 

2860 return self.values 

2861 

2862 @property 

2863 def attrs(self): 

2864 return self.table._v_attrs 

2865 

2866 @property 

2867 def description(self): 

2868 return self.table.description 

2869 

2870 @property 

2871 def col(self): 

2872 """return my current col description""" 

2873 return getattr(self.description, self.cname, None) 

2874 

2875 @property 

2876 def cvalues(self): 

2877 """return my cython values""" 

2878 return self.values 

2879 

2880 def __iter__(self) -> Iterator: 

2881 return iter(self.values) 

2882 

2883 def maybe_set_size(self, min_itemsize=None) -> None: 

2884 """ 

2885 maybe set a string col itemsize: 

2886 min_itemsize can be an integer or a dict with this columns name 

2887 with an integer size 

2888 """ 

2889 if self.kind == "string": 

2890 if isinstance(min_itemsize, dict): 

2891 min_itemsize = min_itemsize.get(self.name) 

2892 

2893 if min_itemsize is not None and self.typ.itemsize < min_itemsize: 

2894 self.typ = _tables().StringCol(itemsize=min_itemsize, pos=self.pos) 

2895 

2896 def validate_names(self) -> None: 

2897 pass 

2898 

2899 def validate_and_set(self, handler: AppendableTable, append: bool) -> None: 

2900 self.table = handler.table 

2901 self.validate_col() 

2902 self.validate_attr(append) 

2903 self.validate_metadata(handler) 

2904 self.write_metadata(handler) 

2905 self.set_attr() 

2906 

2907 def validate_col(self, itemsize=None): 

2908 """validate this column: return the compared against itemsize""" 

2909 # validate this column for string truncation (or reset to the max size) 

2910 if self.kind == "string": 

2911 c = self.col 

2912 if c is not None: 

2913 if itemsize is None: 

2914 itemsize = self.itemsize 

2915 if c.itemsize < itemsize: 

2916 raise ValueError( 

2917 f"Trying to store a string with len [{itemsize}] in " 

2918 f"[{self.cname}] column but\nthis column has a limit of " 

2919 f"[{c.itemsize}]!\nConsider using min_itemsize to " 

2920 "preset the sizes on these columns" 

2921 ) 

2922 return c.itemsize 

2923 

2924 return None 

2925 

2926 def validate_attr(self, append: bool) -> None: 

2927 # check for backwards incompatibility 

2928 if append: 

2929 existing_kind = getattr(self.attrs, self.kind_attr, None) 

2930 if existing_kind is None: 

2931 # brand new table, nothing to be incompatible with 

2932 return 

2933 if existing_kind != self.kind: 

2934 raise TypeError( 

2935 f"incompatible kind in col [{existing_kind} - {self.kind}]" 

2936 ) 

2937 existing_meta = getattr(self.attrs, self.meta_attr, None) 

2938 if existing_meta != self.meta: 

2939 # A CategoricalIndex stores its codes, so both sides are kind 

2940 # "integer" and the check above passes; without this the append 

2941 # silently reinterprets codes as labels or vice versa. GH#65576 

2942 if self.meta == "category": 

2943 raise TypeError( 

2944 "cannot append a categorical index to a non-categorical index" 

2945 ) 

2946 raise TypeError( 

2947 "cannot append a non-categorical index to a categorical index" 

2948 ) 

2949 

2950 def update_info(self, info) -> None: 

2951 """ 

2952 set/update the info for this indexable with the key/value 

2953 if there is a conflict raise/warn as needed 

2954 """ 

2955 for key in self._info_fields: 

2956 value = getattr(self, key, None) 

2957 idx = info.setdefault(self.name, {}) 

2958 

2959 existing_value = idx.get(key) 

2960 if key in idx and value is not None and existing_value != value: 

2961 # frequency/name just warn 

2962 if key in ["freq", "index_name"]: 

2963 ws = attribute_conflict_doc % (key, existing_value, value) 

2964 warnings.warn( 

2965 ws, AttributeConflictWarning, stacklevel=find_stack_level() 

2966 ) 

2967 

2968 # reset 

2969 idx[key] = None 

2970 setattr(self, key, None) 

2971 

2972 else: 

2973 raise ValueError( 

2974 f"invalid info for [{self.name}] for [{key}], " 

2975 f"existing_value [{existing_value}] conflicts with " 

2976 f"new value [{value}]" 

2977 ) 

2978 elif value is not None or existing_value is not None: 

2979 idx[key] = value 

2980 

2981 def set_info(self, info) -> None: 

2982 """set my state from the passed info""" 

2983 idx = info.get(self.name) 

2984 if idx is not None: 

2985 self.__dict__.update(idx) 

2986 

2987 def set_attr(self) -> None: 

2988 """set the kind for this column""" 

2989 _set_attr_if_changed(self.attrs, self.kind_attr, self.kind) 

2990 if self.meta is not None: 

2991 # Persist meta="category" so an all-NaN CategoricalIndex, whose 

2992 # empty categories cannot be written as metadata, still round-trips 

2993 # as categorical rather than as raw integer codes. GH#65576 

2994 _set_attr_if_changed(self.attrs, self.meta_attr, self.meta) 

2995 if self.kind == "string": 

2996 # GH#9604: the NaN sentinel, empty when the index has no missing 

2997 # value, so that its absence marks an older file. Table.indexables 

2998 # reads it back through _read_index_nan_rep. 

2999 _set_attr_if_changed(self.attrs, self.nan_rep_attr, self.nan_rep or "") 

3000 

3001 def validate_metadata(self, handler: AppendableTable) -> None: 

3002 """validate that kind=category does not change the categories""" 

3003 if self.meta == "category": 

3004 new_metadata = self.metadata 

3005 cur_metadata = handler.read_metadata(self.cname) 

3006 if ( 

3007 new_metadata is not None 

3008 and cur_metadata is not None 

3009 and not array_equivalent( 

3010 new_metadata, cur_metadata, strict_nan=True, dtype_equal=True 

3011 ) 

3012 ): 

3013 raise ValueError( 

3014 "cannot append a categorical with different categories " 

3015 "to the existing; align with `cat.set_categories(...)` " 

3016 "or pre-merge with `pd.api.types.union_categoricals` " 

3017 "before writing" 

3018 ) 

3019 

3020 def write_metadata(self, handler: AppendableTable) -> None: 

3021 """set the meta data""" 

3022 if self.metadata is not None: 

3023 handler.write_metadata(self.cname, self.metadata) 

3024 

3025 

3026class GenericIndexCol(IndexCol): 

3027 """an index which is not represented in the data of the table""" 

3028 

3029 @property 

3030 def is_indexed(self) -> bool: 

3031 return False 

3032 

3033 def convert( 

3034 self, values: np.ndarray, nan_rep, encoding: str, errors: str 

3035 ) -> tuple[Index, Index]: 

3036 """ 

3037 Convert the data from this selection to the appropriate pandas type. 

3038 

3039 Parameters 

3040 ---------- 

3041 values : np.ndarray 

3042 nan_rep : str 

3043 encoding : str 

3044 errors : str 

3045 """ 

3046 assert isinstance(values, np.ndarray), type(values) 

3047 

3048 index = RangeIndex(len(values)) 

3049 return index, index 

3050 

3051 def set_attr(self) -> None: 

3052 pass 

3053 

3054 

3055class DataCol(IndexCol): 

3056 """ 

3057 a data holding column, by definition this is not indexable 

3058 

3059 Parameters 

3060 ---------- 

3061 data : the actual data 

3062 cname : the column name in the table to hold the data (typically 

3063 values) 

3064 meta : a string description of the metadata 

3065 metadata : the actual metadata 

3066 """ 

3067 

3068 is_an_indexable = False 

3069 is_data_indexable = False 

3070 _info_fields = ["tz", "ordered"] 

3071 

3072 def __init__( 

3073 self, 

3074 name: str, 

3075 values=None, 

3076 kind=None, 

3077 typ=None, 

3078 cname: str | None = None, 

3079 pos=None, 

3080 tz=None, 

3081 ordered=None, 

3082 table=None, 

3083 meta=None, 

3084 metadata=None, 

3085 dtype: DtypeArg | None = None, 

3086 data=None, 

3087 ) -> None: 

3088 super().__init__( 

3089 name=name, 

3090 values=values, 

3091 kind=kind, 

3092 typ=typ, 

3093 pos=pos, 

3094 cname=cname, 

3095 tz=tz, 

3096 ordered=ordered, 

3097 table=table, 

3098 meta=meta, 

3099 metadata=metadata, 

3100 ) 

3101 self.dtype = dtype 

3102 self.data = data 

3103 

3104 @property 

3105 def dtype_attr(self) -> str: 

3106 return f"{self.name}_dtype" 

3107 

3108 def __repr__(self) -> str: 

3109 temp = tuple( 

3110 map( 

3111 pprint_thing, (self.name, self.cname, self.dtype, self.kind, self.shape) 

3112 ) 

3113 ) 

3114 return ",".join( 

3115 [ 

3116 f"{key}->{value}" 

3117 for key, value in zip( 

3118 ["name", "cname", "dtype", "kind", "shape"], temp, strict=True 

3119 ) 

3120 ] 

3121 ) 

3122 

3123 def __eq__(self, other: object) -> bool: 

3124 """compare 2 col items""" 

3125 return all( 

3126 getattr(self, a, None) == getattr(other, a, None) 

3127 for a in ["name", "cname", "dtype", "pos"] 

3128 ) 

3129 

3130 def set_data(self, data: ArrayLike) -> None: 

3131 assert data is not None 

3132 assert self.dtype is None 

3133 

3134 data, dtype_name = _get_data_and_dtype_name(data) 

3135 

3136 self.data = data 

3137 self.dtype = dtype_name 

3138 self.kind = _dtype_to_kind(dtype_name) 

3139 

3140 def take_data(self): 

3141 """return the data""" 

3142 return self.data 

3143 

3144 @classmethod 

3145 def _get_atom(cls, values: ArrayLike) -> Col: 

3146 """ 

3147 Get an appropriately typed and shaped pytables.Col object for values. 

3148 """ 

3149 dtype = values.dtype 

3150 # GH#26144, GH#38305, GH#42070. CategoricalDtype/DatetimeTZDtype have 

3151 # working code paths below; PeriodDtype is handled in a separate effort. 

3152 if isinstance(dtype, ExtensionDtype) and not isinstance( 

3153 dtype, (CategoricalDtype, DatetimeTZDtype, PeriodDtype) 

3154 ): 

3155 raise NotImplementedError( 

3156 f"Cannot store a column with dtype {dtype} in an HDF5 file. " 

3157 "HDFStore supports NumPy dtypes, datetime64 with timezone, " 

3158 "categorical, and string extension types." 

3159 ) 

3160 itemsize = dtype.itemsize 

3161 

3162 shape = values.shape 

3163 if values.ndim == 1: 

3164 # EA, use block shape pretending it is 2D 

3165 # TODO(EA2D): not necessary with 2D EAs 

3166 shape = (1, values.size) 

3167 

3168 if isinstance(values, Categorical): 

3169 codes = values.codes 

3170 atom = cls.get_atom_data(shape, kind=codes.dtype.name) 

3171 elif lib.is_np_dtype(dtype, "M") or isinstance(dtype, DatetimeTZDtype): 

3172 atom = cls.get_atom_datetime64(shape) 

3173 elif lib.is_np_dtype(dtype, "m"): 

3174 atom = cls.get_atom_timedelta64(shape) 

3175 elif is_complex_dtype(dtype): 

3176 atom = _tables().ComplexCol(itemsize=itemsize, shape=shape[0]) 

3177 elif is_string_dtype(dtype): 

3178 atom = cls.get_atom_string(shape, itemsize) 

3179 else: 

3180 atom = cls.get_atom_data(shape, kind=dtype.name) 

3181 

3182 return atom 

3183 

3184 @classmethod 

3185 def get_atom_string(cls, shape, itemsize): 

3186 return _tables().StringCol(itemsize=itemsize, shape=shape[0]) 

3187 

3188 @classmethod 

3189 def get_atom_coltype(cls, kind: str) -> type[Col]: 

3190 """return the PyTables column class for this column""" 

3191 if kind.startswith("uint"): 

3192 k4 = kind[4:] 

3193 col_name = f"UInt{k4}Col" 

3194 elif kind.startswith("period"): 

3195 # we store as integer 

3196 col_name = "Int64Col" 

3197 else: 

3198 kcap = kind.capitalize() 

3199 col_name = f"{kcap}Col" 

3200 

3201 return getattr(_tables(), col_name) 

3202 

3203 @classmethod 

3204 def get_atom_data(cls, shape, kind: str) -> Col: 

3205 return cls.get_atom_coltype(kind=kind)(shape=shape[0]) 

3206 

3207 @classmethod 

3208 def get_atom_datetime64(cls, shape): 

3209 return _tables().Int64Col(shape=shape[0]) 

3210 

3211 @classmethod 

3212 def get_atom_timedelta64(cls, shape): 

3213 return _tables().Int64Col(shape=shape[0]) 

3214 

3215 @property 

3216 def shape(self): 

3217 return getattr(self.data, "shape", None) 

3218 

3219 @property 

3220 def cvalues(self): 

3221 """return my cython values""" 

3222 return self.data 

3223 

3224 def validate_attr(self, append) -> None: 

3225 """validate that we have the same order as the existing & same dtype""" 

3226 if append: 

3227 existing_fields = getattr(self.attrs, self.kind_attr, None) 

3228 if existing_fields is not None and existing_fields != list(self.values): 

3229 raise ValueError("appended items do not match existing items in table!") 

3230 

3231 existing_dtype = getattr(self.attrs, self.dtype_attr, None) 

3232 if existing_dtype is not None and existing_dtype != self.dtype: 

3233 raise ValueError( 

3234 "appended items dtype do not match existing items dtype in table!" 

3235 ) 

3236 

3237 def convert(self, values: np.ndarray, nan_rep, encoding: str, errors: str): 

3238 """ 

3239 Convert the data from this selection to the appropriate pandas type. 

3240 

3241 Parameters 

3242 ---------- 

3243 values : np.ndarray 

3244 nan_rep : 

3245 encoding : str 

3246 errors : str 

3247 

3248 Returns 

3249 ------- 

3250 index : listlike to become an Index 

3251 data : ndarraylike to become a column 

3252 """ 

3253 assert isinstance(values, np.ndarray), type(values) 

3254 

3255 # values is a recarray 

3256 if values.dtype.fields is not None: 

3257 values = values[self.cname] 

3258 if not values.flags["ALIGNED"]: 

3259 # GH#54396 copy to realign; unaligned buffers SIGBUS 

3260 # on strict-alignment platforms (e.g. 32-bit ARM) 

3261 values = values.copy() 

3262 

3263 assert self.typ is not None 

3264 if self.dtype is None: 

3265 # Note: in tests we never have timedelta64 or datetime64, 

3266 # so the _get_data_and_dtype_name may be unnecessary 

3267 converted, dtype_name = _get_data_and_dtype_name(values) 

3268 kind = _dtype_to_kind(dtype_name) 

3269 else: 

3270 converted = values 

3271 dtype_name = self.dtype 

3272 kind = self.kind 

3273 

3274 assert isinstance(converted, np.ndarray) # for mypy 

3275 

3276 # use the meta if needed 

3277 meta = self.meta 

3278 metadata = self.metadata 

3279 ordered = self.ordered 

3280 tz = self.tz 

3281 

3282 assert dtype_name is not None 

3283 # convert to the correct dtype 

3284 dtype = dtype_name 

3285 

3286 # reverse converts 

3287 if dtype.startswith("datetime64"): 

3288 # recreate with tz if indicated 

3289 if dtype == "datetime64": 

3290 dtype = "datetime64[ns]" 

3291 converted = _set_tz(converted, tz, dtype) 

3292 

3293 elif dtype.startswith("timedelta64"): 

3294 if dtype == "timedelta64": 

3295 # from before we started storing timedelta64 unit 

3296 converted = np.asarray(converted, dtype="m8[ns]") 

3297 else: 

3298 converted = np.asarray(converted, dtype=dtype) 

3299 elif dtype.startswith("period"): 

3300 # GH#41978 PeriodArray values are stored as i8 ordinals; the 

3301 # PyTables atom has shape (1,) per row, so ravel before wrapping. 

3302 pdtype = PeriodDtype.construct_from_string(dtype) 

3303 converted = PeriodArray._simple_new( 

3304 np.asarray(converted, dtype="i8").ravel(), dtype=pdtype 

3305 ) 

3306 elif dtype == "date": 

3307 try: 

3308 converted = np.asarray( 

3309 [date.fromordinal(v) for v in converted], dtype=object 

3310 ) 

3311 except ValueError: 

3312 converted = np.asarray( 

3313 [date.fromtimestamp(v) for v in converted], dtype=object 

3314 ) 

3315 

3316 elif meta == "category": 

3317 # we have a categorical 

3318 categories = metadata 

3319 codes = converted.ravel() 

3320 

3321 # if we have stored a NaN in the categories 

3322 # then strip it; in theory we could have BOTH 

3323 # -1s in the codes and nulls :< 

3324 if categories is None: 

3325 # Handle case of NaN-only categorical columns in which case 

3326 # the categories are an empty array; when this is stored, 

3327 # pytables cannot write a zero-len array, so on readback 

3328 # the categories would be None and `read_hdf()` would fail. 

3329 categories = Index([], dtype=np.float64) 

3330 else: 

3331 mask = isna(categories) 

3332 if mask.any(): 

3333 # A category can be NaN if the nan_rep string was itself 

3334 # a genuine category on write. Drop NaN categories and 

3335 # remap the codes. GH#21741 

3336 remap = np.full(len(categories), -1, dtype=codes.dtype) 

3337 remap[~mask] = np.arange((~mask).sum(), dtype=codes.dtype) 

3338 categories = categories[~mask] 

3339 codes = np.where(codes < 0, codes, remap[codes]) 

3340 

3341 converted = Categorical.from_codes( 

3342 codes, categories=categories, ordered=ordered, validate=False 

3343 ) 

3344 

3345 else: 

3346 try: 

3347 converted = converted.astype(dtype, copy=False) 

3348 except TypeError: 

3349 converted = converted.astype("O", copy=False) 

3350 

3351 # convert nans / decode 

3352 if kind == "string": 

3353 if self.uses_col_nan_rep: 

3354 # GH#9604: None when the column had no missing value, in which 

3355 # case a literal "nan" is left untouched 

3356 col_nan_rep = self.nan_rep 

3357 else: 

3358 # the table-wide nan_rep, which the writer defaulted to "nan" 

3359 col_nan_rep = nan_rep if nan_rep is not None else "nan" 

3360 converted = _unconvert_string_array( 

3361 converted, 

3362 nan_rep=col_nan_rep, 

3363 encoding=encoding, 

3364 errors=errors, 

3365 ) 

3366 

3367 return self.values, converted 

3368 

3369 def set_attr(self) -> None: 

3370 """set the data for this column""" 

3371 _set_attr_if_changed(self.attrs, self.kind_attr, self.values) 

3372 _set_attr_if_changed(self.attrs, self.meta_attr, self.meta) 

3373 assert self.dtype is not None 

3374 _set_attr_if_changed(self.attrs, self.dtype_attr, self.dtype) 

3375 if self.nan_rep is not None: 

3376 # GH#9604: this column's own NaN sentinel (see convert). 

3377 _set_attr_if_changed(self.attrs, self.nan_rep_attr, self.nan_rep) 

3378 

3379 

3380class DataIndexableCol(DataCol): 

3381 """represent a data column that can be indexed""" 

3382 

3383 is_data_indexable = True 

3384 

3385 def validate_names(self) -> None: 

3386 if not is_string_dtype(Index(self.values).dtype): 

3387 # TODO: should the message here be more specifically non-str? 

3388 raise ValueError("cannot have non-object label DataIndexableCol") 

3389 

3390 @classmethod 

3391 def get_atom_string(cls, shape, itemsize): 

3392 return _tables().StringCol(itemsize=itemsize) 

3393 

3394 @classmethod 

3395 def get_atom_data(cls, shape, kind: str) -> Col: 

3396 return cls.get_atom_coltype(kind=kind)() 

3397 

3398 @classmethod 

3399 def get_atom_datetime64(cls, shape): 

3400 return _tables().Int64Col() 

3401 

3402 @classmethod 

3403 def get_atom_timedelta64(cls, shape): 

3404 return _tables().Int64Col() 

3405 

3406 

3407class GenericDataIndexableCol(DataIndexableCol): 

3408 """represent a generic pytables data column""" 

3409 

3410 

3411class Fixed: 

3412 """ 

3413 represent an object in my store 

3414 facilitate read/write of various types of objects 

3415 this is an abstract base class 

3416 

3417 Parameters 

3418 ---------- 

3419 parent : HDFStore 

3420 group : Node 

3421 The group node where the table resides. 

3422 """ 

3423 

3424 pandas_kind: str 

3425 format_type: str = "fixed" # GH#30962 needed by dask 

3426 obj_type: type[DataFrame | Series] 

3427 ndim: int 

3428 parent: HDFStore 

3429 is_table: bool = False 

3430 

3431 def __init__( 

3432 self, 

3433 parent: HDFStore, 

3434 group: Node, 

3435 encoding: str | None = "UTF-8", 

3436 errors: str = "strict", 

3437 ) -> None: 

3438 assert isinstance(parent, HDFStore), type(parent) 

3439 assert _table_mod is not None # needed for mypy 

3440 assert isinstance(group, _table_mod.Node), type(group) 

3441 self.parent = parent 

3442 self.group = group 

3443 self.encoding = _ensure_encoding(encoding) 

3444 self.errors = errors 

3445 

3446 @property 

3447 def pandas_type(self): 

3448 return getattr(self.group._v_attrs, "pandas_type", None) 

3449 

3450 def __repr__(self) -> str: 

3451 """return a pretty representation of myself""" 

3452 self.infer_axes() 

3453 s = self.shape 

3454 if s is not None: 

3455 if isinstance(s, (list, tuple)): 

3456 jshape = ",".join([pprint_thing(x) for x in s]) 

3457 s = f"[{jshape}]" 

3458 return f"{self.pandas_type:12.12} (shape->{s})" 

3459 return self.pandas_type 

3460 

3461 def set_object_info(self) -> None: 

3462 """set my pandas type""" 

3463 self.attrs.pandas_type = str(self.pandas_kind) 

3464 

3465 def copy(self) -> Fixed: 

3466 new_self = copy.copy(self) 

3467 return new_self 

3468 

3469 @property 

3470 def shape(self): 

3471 return self.nrows 

3472 

3473 @property 

3474 def pathname(self): 

3475 return self.group._v_pathname 

3476 

3477 @property 

3478 def _handle(self): 

3479 return self.parent._handle 

3480 

3481 @property 

3482 def _filters(self): 

3483 return self.parent._filters 

3484 

3485 @property 

3486 def _complevel(self) -> int: 

3487 return self.parent._complevel 

3488 

3489 @property 

3490 def _fletcher32(self) -> bool: 

3491 return self.parent._fletcher32 

3492 

3493 @property 

3494 def attrs(self): 

3495 return self.group._v_attrs 

3496 

3497 def set_attrs(self) -> None: 

3498 """set our object attributes""" 

3499 

3500 def get_attrs(self) -> None: 

3501 """get our object attributes""" 

3502 

3503 @property 

3504 def storable(self): 

3505 """return my storable""" 

3506 return self.group 

3507 

3508 @property 

3509 def is_exists(self) -> bool: 

3510 return False 

3511 

3512 @property 

3513 def nrows(self): 

3514 return getattr(self.storable, "nrows", None) 

3515 

3516 def validate(self, other) -> Literal[True] | None: 

3517 """validate against an existing storable""" 

3518 if other is None: 

3519 return None 

3520 return True 

3521 

3522 def infer_axes(self) -> bool: 

3523 """ 

3524 infer the axes of my storer 

3525 return a boolean indicating if we have a valid storer or not 

3526 """ 

3527 s = self.storable 

3528 if s is None: 

3529 return False 

3530 self.get_attrs() 

3531 return True 

3532 

3533 def read( 

3534 self, 

3535 where=None, 

3536 columns=None, 

3537 start: int | None = None, 

3538 stop: int | None = None, 

3539 ) -> Series | DataFrame: 

3540 raise NotImplementedError( 

3541 "cannot read on an abstract storer: subclasses should implement" 

3542 ) 

3543 

3544 def write(self, obj, **kwargs) -> None: 

3545 raise NotImplementedError( 

3546 "cannot write on an abstract storer: subclasses should implement" 

3547 ) 

3548 

3549 def delete( 

3550 self, where=None, start: int | None = None, stop: int | None = None 

3551 ) -> int | None: 

3552 """ 

3553 support fully deleting the node in its entirety (only) - where 

3554 specification must be None 

3555 """ 

3556 if com.all_none(where, start, stop): 

3557 self._handle.remove_node(self.group, recursive=True) 

3558 return None 

3559 

3560 raise TypeError("cannot delete on an abstract storer") 

3561 

3562 

3563class GenericFixed(Fixed): 

3564 """a generified fixed version""" 

3565 

3566 _index_type_map = {DatetimeIndex: "datetime", PeriodIndex: "period"} 

3567 _reverse_index_map = {v: k for k, v in _index_type_map.items()} 

3568 attributes: list[str] = [] 

3569 

3570 # indexer helpers 

3571 def _class_to_alias(self, cls) -> str: 

3572 return self._index_type_map.get(cls, "") 

3573 

3574 def _alias_to_class(self, alias): 

3575 if isinstance(alias, type): # pragma: no cover 

3576 # compat: for a short period of time master stored types 

3577 return alias 

3578 return self._reverse_index_map.get(alias, Index) 

3579 

3580 def _get_index_factory(self, attrs): 

3581 index_class = self._alias_to_class(getattr(attrs, "index_class", "")) 

3582 

3583 factory: Callable 

3584 

3585 kwargs = {} 

3586 if index_class == DatetimeIndex: 

3587 

3588 def f(values, freq=None, tz=None): # pyright: ignore[reportRedeclaration] 

3589 # data are already in UTC, localize and convert if tz present 

3590 dta = DatetimeArray._simple_new(values.values, dtype=values.dtype) 

3591 result = DatetimeIndex._simple_new(dta, name=None) 

3592 result._freq = freq 

3593 if tz is not None: 

3594 result = result.tz_localize("UTC").tz_convert(tz) 

3595 return result 

3596 

3597 factory = f 

3598 elif index_class == PeriodIndex: 

3599 

3600 def f(values, freq=None, tz=None): 

3601 dtype = PeriodDtype(freq) 

3602 parr = PeriodArray._simple_new(values, dtype=dtype) 

3603 return PeriodIndex._simple_new(parr, name=None) 

3604 

3605 factory = f 

3606 else: 

3607 factory = index_class 

3608 kwargs["copy"] = False 

3609 

3610 if "freq" in attrs and attrs["freq"] is not None: 

3611 # GH#33186 old versions of pandas wrote a freq=None attr on every 

3612 # index, including non-datetimelike ones; only a non-None freq is 

3613 # meaningful (and only a TimedeltaIndex reaches here as plain Index). 

3614 freq_attr = attrs["freq"] 

3615 if isinstance(freq_attr, bytes): 

3616 # GH#35917: HDF5 files written by old (Python 2) pandas 

3617 # stored freq as a Python 2 pickle byte-string, which 

3618 # pytables can't unpickle in Python 3. The original 

3619 # freq is unrecoverable, so drop it. The index data 

3620 # itself is unaffected; users can manually reassign 

3621 # via `df.index.freq = df.index.inferred_freq`. 

3622 warnings.warn( 

3623 "Could not decode freq attribute on stored index; " 

3624 "the file was likely written by an older pandas " 

3625 "version. Setting freq=None.", 

3626 stacklevel=find_stack_level(), 

3627 ) 

3628 freq_attr = None 

3629 kwargs["freq"] = freq_attr 

3630 if index_class is Index: 

3631 # DTI/PI would be gotten by _alias_to_class 

3632 factory = TimedeltaIndex 

3633 

3634 if "tz" in attrs: 

3635 kwargs["tz"] = attrs["tz"] 

3636 assert index_class is DatetimeIndex # just checking 

3637 

3638 return factory, kwargs 

3639 

3640 def validate_read(self, columns, where) -> None: 

3641 """ 

3642 raise if any keywords are passed which are not-None 

3643 """ 

3644 if columns is not None: 

3645 raise TypeError( 

3646 "cannot pass a column specification when reading " 

3647 "a Fixed format store. this store must be selected in its entirety" 

3648 ) 

3649 if where is not None: 

3650 raise TypeError( 

3651 "cannot pass a where specification when reading " 

3652 "from a Fixed format store. this store must be selected in its entirety" 

3653 ) 

3654 

3655 @property 

3656 def is_exists(self) -> bool: 

3657 return True 

3658 

3659 def set_attrs(self) -> None: 

3660 """set our object attributes""" 

3661 self.attrs.encoding = self.encoding 

3662 self.attrs.errors = self.errors 

3663 

3664 def get_attrs(self) -> None: 

3665 """retrieve our attributes""" 

3666 self.encoding = _ensure_encoding(getattr(self.attrs, "encoding", None)) 

3667 self.errors = getattr(self.attrs, "errors", "strict") 

3668 for n in self.attributes: 

3669 setattr(self, n, getattr(self.attrs, n, None)) 

3670 

3671 def write(self, obj, **kwargs) -> None: 

3672 self.set_attrs() 

3673 

3674 def read_array(self, key: str, start: int | None = None, stop: int | None = None): 

3675 """read an array for the specified node (off of group""" 

3676 import tables 

3677 

3678 node = getattr(self.group, key) 

3679 attrs = node._v_attrs 

3680 

3681 transposed = getattr(attrs, "transposed", False) 

3682 

3683 if isinstance(node, tables.VLArray): 

3684 ret = node[0][start:stop] 

3685 dtype = getattr(attrs, "value_type", None) 

3686 if dtype is not None: 

3687 ret = pd_array(ret, dtype=dtype) 

3688 else: 

3689 dtype = getattr(attrs, "value_type", None) 

3690 shape = getattr(attrs, "shape", None) 

3691 

3692 if shape is not None: 

3693 # length 0 axis 

3694 ret = np.empty(shape, dtype=dtype) 

3695 else: 

3696 ret = node[start:stop] 

3697 

3698 if dtype and dtype.startswith("datetime64"): 

3699 # reconstruct a timezone if indicated 

3700 if dtype == "datetime64": 

3701 dtype = "datetime64[ns]" 

3702 tz = getattr(attrs, "tz", None) 

3703 ret = _set_tz(ret, tz, dtype) 

3704 

3705 elif dtype and dtype.startswith("timedelta64"): 

3706 if dtype == "timedelta64": 

3707 # This was written back before we started writing 

3708 # timedelta64 units 

3709 ret = np.asarray(ret, dtype="m8[ns]") 

3710 else: 

3711 ret = np.asarray(ret, dtype=dtype) 

3712 

3713 elif dtype and dtype.startswith("period"): 

3714 pdtype = PeriodDtype.construct_from_string(dtype) 

3715 ret = PeriodArray._simple_new(np.asarray(ret, dtype="i8"), dtype=pdtype) 

3716 

3717 if transposed: 

3718 return ret.T 

3719 else: 

3720 return ret 

3721 

3722 def read_index( 

3723 self, key: str, start: int | None = None, stop: int | None = None 

3724 ) -> Index: 

3725 variety = getattr(self.attrs, f"{key}_variety") 

3726 

3727 if variety == "multi": 

3728 return self.read_multi_index(key, start=start, stop=stop) 

3729 elif variety == "regular": 

3730 node = getattr(self.group, key) 

3731 index = self.read_index_node(node, start=start, stop=stop) 

3732 return index 

3733 else: # pragma: no cover 

3734 raise TypeError(f"unrecognized index variety: {variety}") 

3735 

3736 def write_index(self, key: str, index: Index) -> None: 

3737 if isinstance(index, MultiIndex): 

3738 setattr(self.attrs, f"{key}_variety", "multi") 

3739 self.write_multi_index(key, index) 

3740 elif isinstance(index, CategoricalIndex): 

3741 # GH#33909: round-trip a CategoricalIndex by storing its integer 

3742 # codes at this key and the categories at a sibling node. This 

3743 # also avoids _convert_index, which has no fixed-format support 

3744 # for the "category" dtype. 

3745 setattr(self.attrs, f"{key}_variety", "regular") 

3746 cat = index._values 

3747 self.write_array(key, np.asarray(cat.codes)) 

3748 self.write_index(f"{key}_categories", Index(cat.categories)) 

3749 node = getattr(self.group, key) 

3750 node._v_attrs.kind = "category" 

3751 node._v_attrs.name = index.name 

3752 node._v_attrs.ordered = bool(cat.ordered) 

3753 else: 

3754 setattr(self.attrs, f"{key}_variety", "regular") 

3755 converted = _convert_index("index", index, self.encoding, self.errors) 

3756 

3757 self.write_array(key, converted.values) 

3758 

3759 node = getattr(self.group, key) 

3760 node._v_attrs.kind = converted.kind 

3761 node._v_attrs.name = index.name 

3762 

3763 if converted.kind == "string": 

3764 # GH#9604: the NaN sentinel, as in IndexCol.set_attr 

3765 node._v_attrs.nan_rep = converted.nan_rep or "" 

3766 

3767 if isinstance(index, (DatetimeIndex, PeriodIndex)): 

3768 node._v_attrs.index_class = self._class_to_alias(type(index)) 

3769 

3770 if isinstance(index, (DatetimeIndex, PeriodIndex, TimedeltaIndex)): 

3771 node._v_attrs.freq = index.freq 

3772 

3773 if isinstance(index, DatetimeIndex) and index.tz is not None: 

3774 node._v_attrs.tz = _get_tz(index.tz) 

3775 

3776 def write_multi_index(self, key: str, index: MultiIndex) -> None: 

3777 setattr(self.attrs, f"{key}_nlevels", index.nlevels) 

3778 

3779 for i, (lev, level_codes, name) in enumerate( 

3780 zip(index.levels, index.codes, index.names, strict=True) 

3781 ): 

3782 # write the level 

3783 if isinstance(lev.dtype, ExtensionDtype) and not isinstance( 

3784 lev.dtype, StringDtype 

3785 ): 

3786 raise NotImplementedError( 

3787 "Saving a MultiIndex with an extension dtype is not supported." 

3788 ) 

3789 level_key = f"{key}_level{i}" 

3790 conv_level = _convert_index(level_key, lev, self.encoding, self.errors) 

3791 self.write_array(level_key, conv_level.values) 

3792 node = getattr(self.group, level_key) 

3793 node._v_attrs.kind = conv_level.kind 

3794 node._v_attrs.name = name 

3795 

3796 if conv_level.kind == "string": 

3797 # GH#9604: the NaN sentinel, as in IndexCol.set_attr 

3798 node._v_attrs.nan_rep = conv_level.nan_rep or "" 

3799 

3800 # write the name 

3801 setattr(node._v_attrs, f"{key}_name{name}", name) 

3802 

3803 # write the labels 

3804 label_key = f"{key}_label{i}" 

3805 self.write_array(label_key, level_codes) 

3806 

3807 def read_multi_index( 

3808 self, key: str, start: int | None = None, stop: int | None = None 

3809 ) -> MultiIndex: 

3810 nlevels = getattr(self.attrs, f"{key}_nlevels") 

3811 

3812 levels = [] 

3813 codes = [] 

3814 names: list[Hashable] = [] 

3815 for i in range(nlevels): 

3816 level_key = f"{key}_level{i}" 

3817 node = getattr(self.group, level_key) 

3818 # A level holds the unique values, not one entry per row, so it must 

3819 # be read in full; only the codes are sliced by start/stop. 

3820 lev = self.read_index_node(node) 

3821 levels.append(lev) 

3822 names.append(lev.name) 

3823 

3824 label_key = f"{key}_label{i}" 

3825 level_codes = self.read_array(label_key, start=start, stop=stop) 

3826 codes.append(level_codes) 

3827 

3828 return MultiIndex( 

3829 levels=levels, codes=codes, names=names, verify_integrity=True 

3830 ) 

3831 

3832 def read_index_node( 

3833 self, node: Node, start: int | None = None, stop: int | None = None 

3834 ) -> Index: 

3835 data = node[start:stop] 

3836 # If the index was an empty array write_array_empty() will 

3837 # have written a sentinel. Here we replace it with the original. 

3838 if "shape" in node._v_attrs and np.prod(node._v_attrs.shape) == 0: 

3839 data = np.empty(node._v_attrs.shape, dtype=node._v_attrs.value_type) 

3840 kind = node._v_attrs.kind 

3841 name = None 

3842 

3843 if "name" in node._v_attrs: 

3844 name = _ensure_str(node._v_attrs.name) 

3845 

3846 if kind == "category": 

3847 # GH#33909: reconstruct CategoricalIndex from sliced codes plus 

3848 # categories saved at a sibling node by write_index. 

3849 categories = self.read_index(f"{node._v_name}_categories") 

3850 ordered = bool(getattr(node._v_attrs, "ordered", False)) 

3851 cat = Categorical.from_codes( 

3852 data, categories=categories, ordered=ordered, validate=False 

3853 ) 

3854 return CategoricalIndex._simple_new(cat, name=name) 

3855 

3856 attrs = node._v_attrs 

3857 factory, kwargs = self._get_index_factory(attrs) 

3858 

3859 nan_rep = _read_index_nan_rep(attrs) if kind == "string" else None 

3860 

3861 if kind == "string" and using_string_dtype(): 

3862 # GH#9604: once the sentinel is substituted back to NaN, dtype 

3863 # inference can only recover str if some non-missing string 

3864 # survives, so an all-missing index would silently degrade to 

3865 # object. Pin the dtype the values were written with instead. 

3866 kwargs["dtype"] = "str" 

3867 

3868 if kind in ("date", "object"): 

3869 index = factory( 

3870 _unconvert_index( 

3871 data, kind, encoding=self.encoding, errors=self.errors 

3872 ), 

3873 dtype=object, 

3874 **kwargs, 

3875 ) 

3876 else: 

3877 try: 

3878 index = factory( 

3879 _unconvert_index( 

3880 data, 

3881 kind, 

3882 encoding=self.encoding, 

3883 errors=self.errors, 

3884 nan_rep=nan_rep, 

3885 ), 

3886 **kwargs, 

3887 ) 

3888 except UnicodeEncodeError as err: 

3889 if ( 

3890 self.errors == "surrogatepass" 

3891 and using_string_dtype() 

3892 and str(err).endswith("surrogates not allowed") 

3893 and HAS_PYARROW 

3894 ): 

3895 kwargs["dtype"] = StringDtype(storage="python", na_value=np.nan) 

3896 index = factory( 

3897 _unconvert_index( 

3898 data, 

3899 kind, 

3900 encoding=self.encoding, 

3901 errors=self.errors, 

3902 nan_rep=nan_rep, 

3903 ), 

3904 **kwargs, 

3905 ) 

3906 else: 

3907 raise 

3908 

3909 index.name = name 

3910 

3911 return index 

3912 

3913 def write_array_empty(self, key: str, value: ArrayLike) -> None: 

3914 """write a 0-len array""" 

3915 # ugly hack for length 0 axes 

3916 arr = np.empty((1,) * value.ndim) 

3917 self._handle.create_array(self.group, key, arr) 

3918 node = getattr(self.group, key) 

3919 node._v_attrs.value_type = str(value.dtype) 

3920 node._v_attrs.shape = value.shape 

3921 

3922 def write_array( 

3923 self, key: str, obj: AnyArrayLike, items: Index | None = None 

3924 ) -> None: 

3925 value = extract_array(obj, extract_numpy=True) 

3926 

3927 if key in self.group: 

3928 self._handle.remove_node(self.group, key) 

3929 

3930 # Transform needed to interface with pytables row/col notation 

3931 empty_array = value.size == 0 

3932 transposed = False 

3933 

3934 if isinstance(value.dtype, CategoricalDtype): 

3935 raise NotImplementedError( 

3936 "Cannot store a category dtype in an HDF5 dataset that uses format=" 

3937 '"fixed". Use format="table".' 

3938 ) 

3939 if not empty_array: 

3940 if hasattr(value, "T"): 

3941 # ExtensionArrays (1d) may not have transpose. 

3942 value = value.T 

3943 transposed = True 

3944 

3945 if isinstance(value, BaseStringArray): 

3946 # GH#64180: BaseStringArray must use the VLArray path. 

3947 # Atom.from_dtype does not handle ExtensionDtype. 

3948 vlarr = self._handle.create_vlarray( 

3949 self.group, key, _tables().ObjectAtom(), filters=self._filters 

3950 ) 

3951 vlarr.append(value.to_numpy()) 

3952 node = getattr(self.group, key) 

3953 node._v_attrs.value_type = str(value.dtype) 

3954 

3955 else: 

3956 atom = None 

3957 if self._filters is not None: 

3958 with suppress(ValueError): 

3959 # get the atom for this datatype 

3960 atom = _tables().Atom.from_dtype(value.dtype) 

3961 

3962 if atom is not None: 

3963 # We only get here if self._filters is non-None and 

3964 # the Atom.from_dtype call succeeded 

3965 

3966 # create an empty chunked array and fill it from value 

3967 if not empty_array: 

3968 ca = self._handle.create_carray( 

3969 self.group, key, atom, value.shape, filters=self._filters 

3970 ) 

3971 ca[:] = value 

3972 

3973 else: 

3974 self.write_array_empty(key, value) 

3975 

3976 elif value.dtype.type == np.object_: 

3977 # infer the type, warn if we have a non-string type here 

3978 # (for performance) 

3979 inferred_type = lib.infer_dtype(value, skipna=False) 

3980 if empty_array: 

3981 pass 

3982 elif inferred_type == "string": 

3983 pass 

3984 elif config["mode"]["performance_warnings"]: 

3985 # GH#28460 a single object block may hold several columns; 

3986 # only flag the ones that are not plain strings, since a 

3987 # string-only column would not warn on its own. 

3988 if value.ndim == 2 and items is not None: 

3989 block = cast("np.ndarray", value) 

3990 mask = [ 

3991 not lib.is_string_array(block[:, j], skipna=False) 

3992 for j in range(block.shape[1]) 

3993 ] 

3994 items = items[mask] 

3995 ws = performance_doc % (inferred_type, key, items) 

3996 warnings.warn(ws, PerformanceWarning, stacklevel=find_stack_level()) 

3997 

3998 vlarr = self._handle.create_vlarray( 

3999 self.group, key, _tables().ObjectAtom(), filters=self._filters 

4000 ) 

4001 vlarr.append(value) 

4002 

4003 elif lib.is_np_dtype(value.dtype, "M"): 

4004 self._handle.create_array(self.group, key, value.view("i8")) 

4005 getattr(self.group, key)._v_attrs.value_type = str(value.dtype) 

4006 elif isinstance(value.dtype, DatetimeTZDtype): 

4007 # store as UTC 

4008 # with a zone 

4009 

4010 # error: "ExtensionArray" has no attribute "asi8" 

4011 self._handle.create_array( 

4012 self.group, 

4013 key, 

4014 value.asi8, # type: ignore[attr-defined] 

4015 ) 

4016 

4017 node = getattr(self.group, key) 

4018 # error: "ExtensionArray" has no attribute "tz" 

4019 node._v_attrs.tz = _get_tz(value.tz) # type: ignore[attr-defined] 

4020 node._v_attrs.value_type = f"datetime64[{value.dtype.unit}]" 

4021 elif lib.is_np_dtype(value.dtype, "m"): 

4022 self._handle.create_array(self.group, key, value.view("i8")) 

4023 getattr(self.group, key)._v_attrs.value_type = str(value.dtype) 

4024 elif isinstance(value.dtype, PeriodDtype): 

4025 # GH#41978 store PeriodArray as i8 ordinals + freq attr 

4026 # error: "ExtensionArray" has no attribute "asi8" 

4027 self._handle.create_array( 

4028 self.group, 

4029 key, 

4030 value.asi8, # type: ignore[attr-defined] 

4031 ) 

4032 node = getattr(self.group, key) 

4033 node._v_attrs.value_type = str(value.dtype) 

4034 elif empty_array: 

4035 self.write_array_empty(key, value) 

4036 else: 

4037 # GH#26144, GH#38305, GH#42070. PeriodDtype intentionally falls 

4038 # through here; it is handled in a separate effort. 

4039 if isinstance(value.dtype, ExtensionDtype) and not isinstance( 

4040 value.dtype, PeriodDtype 

4041 ): 

4042 raise NotImplementedError( 

4043 f"Cannot store a column with dtype {value.dtype} in " 

4044 'an HDF5 file with format="fixed". HDFStore supports ' 

4045 "NumPy dtypes, datetime64 with timezone, categorical, " 

4046 "and string extension types." 

4047 ) 

4048 self._handle.create_array(self.group, key, value) 

4049 

4050 getattr(self.group, key)._v_attrs.transposed = transposed 

4051 

4052 

4053class SeriesFixed(GenericFixed): 

4054 pandas_kind = "series" 

4055 attributes = ["name"] 

4056 

4057 name: Hashable 

4058 

4059 @property 

4060 def shape(self) -> tuple[int] | None: 

4061 try: 

4062 node = self.group.values 

4063 if "shape" in node._v_attrs: 

4064 # GH#37235 an empty array is stored as a length-1 sentinel 

4065 # (see write_array_empty); the true shape is in this attr. 

4066 return tuple(node._v_attrs.shape) 

4067 return (len(node),) 

4068 except (TypeError, AttributeError): 

4069 return None 

4070 

4071 def read( 

4072 self, 

4073 where=None, 

4074 columns=None, 

4075 start: int | None = None, 

4076 stop: int | None = None, 

4077 ) -> Series: 

4078 self.validate_read(columns, where) 

4079 index = self.read_index("index", start=start, stop=stop) 

4080 values = self.read_array("values", start=start, stop=stop) 

4081 try: 

4082 result = Series(values, index=index, name=self.name, copy=False) 

4083 except UnicodeEncodeError as err: 

4084 if ( 

4085 self.errors == "surrogatepass" 

4086 and using_string_dtype() 

4087 and str(err).endswith("surrogates not allowed") 

4088 and HAS_PYARROW 

4089 ): 

4090 result = Series( 

4091 values, 

4092 index=index, 

4093 name=self.name, 

4094 copy=False, 

4095 dtype=StringDtype(storage="python", na_value=np.nan), 

4096 ) 

4097 else: 

4098 raise 

4099 return result 

4100 

4101 def write(self, obj, **kwargs) -> None: 

4102 super().write(obj, **kwargs) 

4103 self.write_index("index", obj.index) 

4104 self.write_array("values", obj) 

4105 self.attrs.name = obj.name 

4106 

4107 

4108class BlockManagerFixed(GenericFixed): 

4109 attributes = ["ndim", "nblocks"] 

4110 

4111 nblocks: int 

4112 

4113 @property 

4114 def shape(self) -> list[int] | None: 

4115 try: 

4116 ndim = self.ndim 

4117 

4118 # items 

4119 items = 0 

4120 for i in range(self.nblocks): 

4121 node = getattr(self.group, f"block{i}_items") 

4122 shape = getattr(node, "shape", None) 

4123 if shape is not None: 

4124 items += shape[0] 

4125 

4126 # data shape 

4127 node = self.group.block0_values 

4128 data_shape: tuple[Any, ...] | None 

4129 if "shape" in node._v_attrs: 

4130 # GH#37235 an empty block is stored un-transposed as a 

4131 # (1,)*ndim sentinel (see write_array_empty), with the true 

4132 # shape in this attr. Reverse it to match the transposed 

4133 # layout used for non-empty blocks. 

4134 data_shape = tuple(reversed(node._v_attrs.shape)) 

4135 else: 

4136 data_shape = getattr(node, "shape", None) 

4137 if data_shape is not None: 

4138 shape = list(data_shape[0 : (ndim - 1)]) 

4139 else: 

4140 shape = [] 

4141 

4142 shape.append(items) 

4143 

4144 return shape 

4145 except AttributeError: 

4146 return None 

4147 

4148 def read( 

4149 self, 

4150 where=None, 

4151 columns=None, 

4152 start: int | None = None, 

4153 stop: int | None = None, 

4154 ) -> DataFrame: 

4155 # start, stop applied to rows, so 0th axis only 

4156 self.validate_read(columns, where) 

4157 select_axis = self.obj_type()._get_block_manager_axis(0) 

4158 

4159 axes = [] 

4160 for i in range(self.ndim): 

4161 _start, _stop = (start, stop) if i == select_axis else (None, None) 

4162 ax = self.read_index(f"axis{i}", start=_start, stop=_stop) 

4163 axes.append(ax) 

4164 

4165 items = axes[0] 

4166 dfs = [] 

4167 

4168 for i in range(self.nblocks): 

4169 blk_items = self.read_index(f"block{i}_items") 

4170 values = self.read_array(f"block{i}_values", start=_start, stop=_stop) 

4171 

4172 columns = items[items.get_indexer(blk_items)] 

4173 arr = values.T 

4174 if isinstance(arr, np.ndarray): 

4175 # DataFrame stores the block as arr.T, so pass a Fortran-ordered 

4176 # arr to get a C-contiguous block (column-major DataFrame), so 

4177 # per-column access is contiguous (GH#22073, GH#60469). 

4178 arr = np.asfortranarray(arr) 

4179 df = DataFrame(arr, columns=columns, index=axes[1], copy=False) 

4180 if ( 

4181 using_string_dtype() 

4182 and isinstance(values, np.ndarray) 

4183 and is_string_array(values, skipna=True) 

4184 ): 

4185 df = df.astype(StringDtype(na_value=np.nan)) 

4186 dfs.append(df) 

4187 

4188 if len(dfs) > 0: 

4189 out = concat(dfs, axis=1) 

4190 return out.reindex(columns=items) 

4191 

4192 return DataFrame(columns=axes[0], index=axes[1]) 

4193 

4194 def write(self, obj, **kwargs) -> None: 

4195 super().write(obj, **kwargs) 

4196 

4197 data = obj._mgr 

4198 if not data.is_consolidated(): 

4199 data = data.consolidate() 

4200 

4201 self.attrs.ndim = data.ndim 

4202 for i, ax in enumerate(data.axes): 

4203 if i == 0 and (not ax.is_unique): 

4204 raise ValueError("Columns index has to be unique for fixed format") 

4205 self.write_index(f"axis{i}", ax) 

4206 

4207 # Supporting mixed-type DataFrame objects...nontrivial 

4208 self.attrs.nblocks = len(data.blocks) 

4209 for i, blk in enumerate(data.blocks): 

4210 # I have no idea why, but writing values before items fixed #2299 

4211 blk_items = data.items.take(blk.mgr_locs) 

4212 self.write_array(f"block{i}_values", blk.values, items=blk_items) 

4213 self.write_index(f"block{i}_items", blk_items) 

4214 

4215 

4216class FrameFixed(BlockManagerFixed): 

4217 pandas_kind = "frame" 

4218 obj_type = DataFrame 

4219 

4220 

4221class Table(Fixed): 

4222 """ 

4223 represent a table: 

4224 facilitate read/write of various types of tables 

4225 

4226 Attrs in Table Node 

4227 ------------------- 

4228 These are attributes that are store in the main table node, they are 

4229 necessary to recreate these tables when read back in. 

4230 

4231 index_axes : a list of tuples of the (original indexing axis and 

4232 index column) 

4233 non_index_axes: a list of tuples of the (original index axis and 

4234 columns on a non-indexing axis) 

4235 values_axes : a list of the columns which comprise the data of this 

4236 table 

4237 data_columns : a list of the columns that we are allowing indexing 

4238 (these become single columns in values_axes) 

4239 nan_rep : the string to use for nan representations for string 

4240 objects 

4241 levels : the names of levels 

4242 metadata : the names of the metadata columns 

4243 """ 

4244 

4245 pandas_kind = "wide_table" 

4246 format_type: str = "table" # GH#30962 needed by dask 

4247 table_type: str 

4248 levels: int | list[Hashable] = 1 # pyright: ignore[reportRedeclaration] 

4249 is_table = True 

4250 

4251 metadata: list 

4252 

4253 def __init__( 

4254 self, 

4255 parent: HDFStore, 

4256 group: Node, 

4257 encoding: str | None = None, 

4258 errors: str = "strict", 

4259 index_axes: list[IndexCol] | None = None, 

4260 non_index_axes: list[tuple[AxisInt, Any]] | None = None, 

4261 values_axes: list[DataCol] | None = None, 

4262 data_columns: list | None = None, 

4263 info: dict | None = None, 

4264 nan_rep=None, 

4265 ) -> None: 

4266 super().__init__(parent, group, encoding=encoding, errors=errors) 

4267 self.index_axes = index_axes or [] 

4268 self.non_index_axes = non_index_axes or [] 

4269 self.values_axes = values_axes or [] 

4270 self.data_columns = data_columns or [] 

4271 self.info = info or {} 

4272 self.nan_rep = nan_rep 

4273 

4274 @property 

4275 def table_type_short(self) -> str: 

4276 return self.table_type.split("_")[0] 

4277 

4278 def __repr__(self) -> str: 

4279 """return a pretty representation of myself""" 

4280 self.infer_axes() 

4281 jdc = ",".join(self.data_columns) if len(self.data_columns) else "" 

4282 dc = f",dc->[{jdc}]" 

4283 

4284 jindex_axes = ",".join([a.name for a in self.index_axes]) 

4285 return ( 

4286 f"{self.pandas_type:12.12} " 

4287 f"(typ->{self.table_type_short},nrows->{self.nrows}," 

4288 f"ncols->{self.ncols},indexers->[{jindex_axes}]{dc})" 

4289 ) 

4290 

4291 def __getitem__(self, c: str): 

4292 """return the axis for c""" 

4293 for a in self.axes: 

4294 if c == a.name: 

4295 return a 

4296 return None 

4297 

4298 def validate(self, other) -> None: 

4299 """validate against an existing table""" 

4300 if other is None: 

4301 return 

4302 

4303 if other.table_type != self.table_type: 

4304 raise TypeError( 

4305 "incompatible table_type with existing " 

4306 f"[{other.table_type} - {self.table_type}]" 

4307 ) 

4308 

4309 for c in ["index_axes", "non_index_axes", "values_axes"]: 

4310 sv = getattr(self, c, None) 

4311 ov = getattr(other, c, None) 

4312 if sv != ov: 

4313 # show the error for the specific axes 

4314 # Argument 1 to "enumerate" has incompatible type 

4315 # "Optional[Any]"; expected "Iterable[Any]" [arg-type] 

4316 for i, sax in enumerate(sv): # type: ignore[arg-type] 

4317 # Value of type "Optional[Any]" is not indexable [index] 

4318 oax = ov[i] # type: ignore[index] 

4319 if sax != oax: 

4320 if c == "values_axes" and sax.kind != oax.kind: 

4321 raise ValueError( 

4322 f"Cannot serialize the column [{oax.values[0]}] " 

4323 f"because its data contents are not [{sax.kind}] " 

4324 f"but [{oax.kind}] object dtype" 

4325 ) 

4326 raise ValueError( 

4327 f"invalid combination of [{c}] on appending data " 

4328 f"[{sax}] vs current table [{oax}]" 

4329 ) 

4330 

4331 # should never get here 

4332 raise Exception( 

4333 f"invalid combination of [{c}] on appending data [{sv}] vs " 

4334 f"current table [{ov}]" 

4335 ) 

4336 

4337 @property 

4338 def is_multi_index(self) -> bool: 

4339 """the levels attribute is 1 or a list in the case of a multi-index""" 

4340 return isinstance(self.levels, list) 

4341 

4342 def validate_multiindex( 

4343 self, obj: DataFrame | Series 

4344 ) -> tuple[DataFrame, list[Hashable]]: 

4345 """ 

4346 validate that we can store the multi-index; reset and return the 

4347 new object 

4348 """ 

4349 levels = com.fill_missing_names(obj.index.names) 

4350 if "index" in levels: 

4351 # GH#6208 'index' is reserved as the implicit row-index name 

4352 # in the table format and collides with a level named 'index'. 

4353 raise ValueError( 

4354 "cannot store a MultiIndex with a level named 'index' as a " 

4355 "table; 'index' is reserved for the implicit row index" 

4356 ) 

4357 try: 

4358 reset_obj = obj.reset_index() 

4359 except ValueError as err: 

4360 raise ValueError( 

4361 "duplicate names/columns in the multi-index when storing as a table" 

4362 ) from err 

4363 assert isinstance(reset_obj, DataFrame) # for mypy 

4364 return reset_obj, levels 

4365 

4366 @property 

4367 def nrows_expected(self) -> int: 

4368 """based on our axes, compute the expected nrows""" 

4369 return np.prod([i.cvalues.shape[0] for i in self.index_axes]) 

4370 

4371 @property 

4372 def is_exists(self) -> bool: 

4373 """has this table been created""" 

4374 return "table" in self.group 

4375 

4376 @property 

4377 def storable(self): 

4378 return getattr(self.group, "table", None) 

4379 

4380 @property 

4381 def table(self): 

4382 """return the table group (this is my storable)""" 

4383 return self.storable 

4384 

4385 @property 

4386 def dtype(self): 

4387 return self.table.dtype 

4388 

4389 @property 

4390 def description(self): 

4391 return self.table.description 

4392 

4393 @property 

4394 def axes(self) -> itertools.chain[IndexCol]: 

4395 return itertools.chain(self.index_axes, self.values_axes) 

4396 

4397 @property 

4398 def ncols(self) -> int: 

4399 """the number of total columns in the values axes""" 

4400 return sum(len(a.values) for a in self.values_axes) 

4401 

4402 @property 

4403 def is_transposed(self) -> bool: 

4404 return False 

4405 

4406 @property 

4407 def data_orientation(self) -> tuple[int, ...]: 

4408 """return a tuple of my permutated axes, non_indexable at the front""" 

4409 return tuple( 

4410 itertools.chain( 

4411 [int(a[0]) for a in self.non_index_axes], 

4412 [int(a.axis) for a in self.index_axes], 

4413 ) 

4414 ) 

4415 

4416 def queryables(self) -> dict[str, Any]: 

4417 """return a dict of the kinds allowable columns for this object""" 

4418 # mypy doesn't recognize DataFrame._AXIS_NAMES, so we re-write it here 

4419 axis_names = {0: "index", 1: "columns"} 

4420 

4421 # compute the values_axes queryables 

4422 d1 = [(a.cname, a) for a in self.index_axes] 

4423 d2 = [(axis_names[axis], None) for axis, values in self.non_index_axes] 

4424 d3 = [ 

4425 (v.cname, v) for v in self.values_axes if v.name in set(self.data_columns) 

4426 ] 

4427 

4428 return dict(d1 + d2 + d3) 

4429 

4430 def index_cols(self) -> list[tuple[Any, Any]]: 

4431 """return a list of my index cols""" 

4432 # Note: each `i.cname` below is assured to be a str. 

4433 return [(i.axis, i.cname) for i in self.index_axes] 

4434 

4435 def values_cols(self) -> list[str]: 

4436 """return a list of my values cols""" 

4437 return [i.cname for i in self.values_axes] 

4438 

4439 def _get_metadata_path(self, key: str) -> str: 

4440 """return the metadata pathname for this key""" 

4441 group = self.group._v_pathname 

4442 return f"{group}/meta/{key}/meta" 

4443 

4444 def write_metadata(self, key: str, values: np.ndarray) -> None: 

4445 """ 

4446 Write out a metadata array to the key as a fixed-format Series. 

4447 

4448 Parameters 

4449 ---------- 

4450 key : str 

4451 values : ndarray 

4452 """ 

4453 if len(values) == 0: 

4454 # PyTables cannot store a zero-len array; the read path already 

4455 # treats a missing metadata node as an empty categories Index, 

4456 # so skipping the write is the contract we've relied on. 

4457 return 

4458 self.parent.put( 

4459 self._get_metadata_path(key), 

4460 Series(values, copy=False), 

4461 format="table", 

4462 encoding=self.encoding, 

4463 errors=self.errors, 

4464 nan_rep=self.nan_rep, 

4465 track_times=False, 

4466 ) 

4467 

4468 def read_metadata(self, key: str): 

4469 """return the meta data array for this key""" 

4470 if getattr(getattr(self.group, "meta", None), key, None) is not None: 

4471 return self.parent.select(self._get_metadata_path(key)) 

4472 return None 

4473 

4474 def set_attrs(self) -> None: 

4475 """set our table type & indexables""" 

4476 self.attrs.table_type = str(self.table_type) 

4477 self.attrs.index_cols = self.index_cols() 

4478 self.attrs.values_cols = self.values_cols() 

4479 self.attrs.non_index_axes = self.non_index_axes 

4480 self.attrs.data_columns = self.data_columns 

4481 self.attrs.nan_rep = self.nan_rep 

4482 self.attrs.encoding = self.encoding 

4483 self.attrs.errors = self.errors 

4484 self.attrs.levels = self.levels 

4485 # GH#9604: marks that string MultiIndex levels are stored with a 

4486 # per-level NaN sentinel (older files lack it and read back as before). 

4487 # self.levels is the int default 1 for non-MI tables, a list for MI. 

4488 self.attrs.mi_level_nan_rep = isinstance(self.levels, list) 

4489 # GH#9604: every string data column is stored with its own NaN 

4490 # sentinel, superseding mi_level_nan_rep. A caller-supplied nan_rep 

4491 # keeps its documented table-wide meaning, so such a table opts out. 

4492 self.attrs.data_col_nan_rep = self.nan_rep is None 

4493 self.attrs.info = self.info 

4494 

4495 def get_attrs(self) -> None: 

4496 """retrieve our attributes""" 

4497 self.non_index_axes = getattr(self.attrs, "non_index_axes", None) or [] 

4498 self.data_columns = getattr(self.attrs, "data_columns", None) or [] 

4499 self.info = getattr(self.attrs, "info", None) or {} 

4500 self.nan_rep = getattr(self.attrs, "nan_rep", None) 

4501 self.encoding = _ensure_encoding(getattr(self.attrs, "encoding", None)) 

4502 self.errors = getattr(self.attrs, "errors", "strict") 

4503 self.levels: list[Hashable] = getattr(self.attrs, "levels", None) or [] # pyright: ignore[reportRedeclaration] 

4504 self.index_axes = [a for a in self.indexables if a.is_an_indexable] 

4505 self.values_axes = [a for a in self.indexables if not a.is_an_indexable] 

4506 

4507 def validate_min_itemsize(self, min_itemsize) -> None: 

4508 """ 

4509 validate the min_itemsize doesn't contain items that are not in the 

4510 axes this needs data_columns to be defined 

4511 """ 

4512 if min_itemsize is None: 

4513 return 

4514 if not isinstance(min_itemsize, dict): 

4515 return 

4516 

4517 q = self.queryables() 

4518 for k in min_itemsize: 

4519 # ok, apply generally 

4520 if k == "values": 

4521 continue 

4522 if k not in q: 

4523 raise ValueError( 

4524 f"min_itemsize has the key [{k}] which is not an axis or " 

4525 "data_column" 

4526 ) 

4527 

4528 @cache_readonly 

4529 def indexables(self): 

4530 """create/cache the indexables if they don't exist""" 

4531 _indexables = [] 

4532 

4533 desc = self.description 

4534 table_attrs = self.table.attrs 

4535 # GH#9604: levels / the MultiIndex-level marker live on the group attrs 

4536 # (self.attrs), not the table-node attrs used for per-column metadata. 

4537 levels_attr = getattr(self.attrs, "levels", None) 

4538 levels = levels_attr if isinstance(levels_attr, list) else [] 

4539 mi_level_marker = getattr(self.attrs, "mi_level_nan_rep", False) 

4540 data_col_marker = getattr(self.attrs, "data_col_nan_rep", False) 

4541 

4542 # Note: each of the `name` kwargs below are str, ensured 

4543 # by the definition in index_cols. 

4544 # index columns 

4545 for i, (axis, name) in enumerate(self.attrs.index_cols): 

4546 atom = getattr(desc, name) 

4547 md = self.read_metadata(name) 

4548 # Prefer the explicit meta attribute (written since GH#65576); it 

4549 # survives even when the categories are empty and no metadata node 

4550 # exists. Fall back to inferring from the metadata node for files 

4551 # written before the attribute was persisted. 

4552 meta = getattr(table_attrs, f"{name}_meta", None) 

4553 if meta is None and md is not None: 

4554 meta = "category" 

4555 

4556 kind_attr = f"{name}_kind" 

4557 kind = getattr(table_attrs, kind_attr, None) 

4558 nan_rep = ( 

4559 _read_index_nan_rep(table_attrs, f"{name}_nan_rep") 

4560 if kind == "string" 

4561 else None 

4562 ) 

4563 

4564 index_col = IndexCol( 

4565 name=name, 

4566 axis=axis, 

4567 pos=i, 

4568 kind=kind, 

4569 typ=atom, 

4570 table=self.table, 

4571 meta=meta, 

4572 metadata=md, 

4573 nan_rep=nan_rep, 

4574 ordered=self.info.get(name, {}).get("ordered"), 

4575 ) 

4576 _indexables.append(index_col) 

4577 

4578 # values columns 

4579 dc = set(self.data_columns) 

4580 base_pos = len(_indexables) 

4581 

4582 def f(i, c: str) -> DataCol: 

4583 assert isinstance(c, str) 

4584 klass = DataCol 

4585 if c in dc: 

4586 klass = DataIndexableCol 

4587 

4588 atom = getattr(desc, c) 

4589 

4590 # TODO: why kind_attr here? 

4591 values = getattr(table_attrs, f"{c}_kind", None) 

4592 dtype = getattr(table_attrs, f"{c}_dtype", None) 

4593 # Argument 1 to "_dtype_to_kind" has incompatible type 

4594 # "Optional[Any]"; expected "str" [arg-type] 

4595 kind = _dtype_to_kind(dtype) # type: ignore[arg-type] 

4596 

4597 md = self.read_metadata(c) 

4598 # TODO: figure out why these two versions of `meta` dont always match. 

4599 # meta = "category" if md is not None else None 

4600 meta = getattr(table_attrs, f"{c}_meta", None) 

4601 

4602 # GH#9604: mi_level_nan_rep predates data_col_nan_rep and covers 

4603 # only the MultiIndex levels, which keep a sentinel even when the 

4604 # caller supplied a nan_rep for the data columns. 

4605 uses_col_nan_rep = data_col_marker or (c in levels and mi_level_marker) 

4606 nan_rep = ( 

4607 getattr(table_attrs, f"{c}_nan_rep", None) if uses_col_nan_rep else None 

4608 ) 

4609 

4610 obj = klass( 

4611 name=c, 

4612 cname=c, 

4613 values=values, 

4614 kind=kind, 

4615 pos=base_pos + i, 

4616 typ=atom, 

4617 table=self.table, 

4618 meta=meta, 

4619 metadata=md, 

4620 dtype=dtype, 

4621 ordered=self.info.get(c, {}).get("ordered"), 

4622 ) 

4623 obj.uses_col_nan_rep = uses_col_nan_rep 

4624 obj.nan_rep = nan_rep 

4625 return obj 

4626 

4627 # Note: the definition of `values_cols` ensures that each 

4628 # `c` below is a str. 

4629 _indexables.extend([f(i, c) for i, c in enumerate(self.attrs.values_cols)]) 

4630 

4631 return _indexables 

4632 

4633 def create_index( 

4634 self, columns=None, optlevel=None, kind: str | None = None 

4635 ) -> None: 

4636 """ 

4637 Create a pytables index on the specified columns. 

4638 

4639 Parameters 

4640 ---------- 

4641 columns : None, bool, or listlike[str] 

4642 Indicate which columns to create an index on. 

4643 

4644 * False : Do not create any indexes. 

4645 * True : Create indexes on all columns. 

4646 * None : Create indexes on all columns. 

4647 * listlike : Create indexes on the given columns. 

4648 

4649 optlevel : int or None, default None 

4650 Optimization level, if None, pytables defaults to 6. 

4651 kind : str or None, default None 

4652 Kind of index, if None, pytables defaults to "medium". 

4653 

4654 Raises 

4655 ------ 

4656 TypeError if trying to create an index on a complex-type column. 

4657 

4658 Notes 

4659 ----- 

4660 Cannot index Time64Col or ComplexCol. 

4661 Pytables must be >= 3.0. 

4662 """ 

4663 if not self.infer_axes(): 

4664 return 

4665 if columns is False: 

4666 return 

4667 

4668 # index all indexables and data_columns 

4669 if columns is None or columns is True: 

4670 columns = [a.cname for a in self.axes if a.is_data_indexable] 

4671 if not isinstance(columns, (tuple, list)): 

4672 columns = [columns] 

4673 

4674 kw = {} 

4675 if optlevel is not None: 

4676 kw["optlevel"] = optlevel 

4677 if kind is not None: 

4678 kw["kind"] = kind 

4679 

4680 table = self.table 

4681 for c in columns: 

4682 v = getattr(table.cols, c, None) 

4683 if v is not None: 

4684 # remove the index if the kind/optlevel have changed 

4685 if v.is_indexed: 

4686 index = v.index 

4687 cur_optlevel = index.optlevel 

4688 cur_kind = index.kind 

4689 

4690 if kind is not None and cur_kind != kind: 

4691 v.remove_index() 

4692 else: 

4693 kw["kind"] = cur_kind 

4694 

4695 if optlevel is not None and cur_optlevel != optlevel: 

4696 v.remove_index() 

4697 else: 

4698 kw["optlevel"] = cur_optlevel 

4699 

4700 # create the index 

4701 if not v.is_indexed: 

4702 if v.type.startswith("complex"): 

4703 raise TypeError( 

4704 "Columns containing complex values can be stored but " 

4705 "cannot be indexed when using table format. Either use " 

4706 "fixed format, set index=False, or do not include " 

4707 "the columns containing complex values to " 

4708 "data_columns when initializing the table." 

4709 ) 

4710 v.create_index(**kw) 

4711 elif c in self.non_index_axes[0][1]: 

4712 # GH 28156 

4713 raise AttributeError( 

4714 f"column {c} is not a data_column.\n" 

4715 f"In order to read column {c} you must reload the dataframe \n" 

4716 f"into HDFStore and include {c} with the data_columns argument." 

4717 ) 

4718 

4719 def _read_axes( 

4720 self, where, start: int | None = None, stop: int | None = None 

4721 ) -> list[tuple[np.ndarray, np.ndarray] | tuple[Index, Index]]: 

4722 """ 

4723 Create the axes sniffed from the table. 

4724 

4725 Parameters 

4726 ---------- 

4727 where : ??? 

4728 start : int or None, default None 

4729 stop : int or None, default None 

4730 

4731 Returns 

4732 ------- 

4733 List[Tuple[index_values, column_values]] 

4734 """ 

4735 # create the selection 

4736 selection = Selection(self, where=where, start=start, stop=stop) 

4737 values = selection.select() 

4738 

4739 results = [] 

4740 # convert the data 

4741 for a in self.axes: 

4742 a.set_info(self.info) 

4743 res = a.convert( 

4744 values, 

4745 nan_rep=self.nan_rep, 

4746 encoding=self.encoding, 

4747 errors=self.errors, 

4748 ) 

4749 results.append(res) 

4750 

4751 return results 

4752 

4753 @classmethod 

4754 def get_object(cls, obj, transposed: bool): 

4755 """return the data for this obj""" 

4756 return obj 

4757 

4758 def validate_data_columns( 

4759 self, data_columns, min_itemsize, non_index_axes, index_cnames=() 

4760 ) -> list: 

4761 """ 

4762 take the input data_columns and min_itemize and create a data 

4763 columns spec 

4764 """ 

4765 if not len(non_index_axes): 

4766 return [] 

4767 

4768 axis, axis_labels = non_index_axes[0] 

4769 info = self.info.get(axis, {}) 

4770 if info.get("type") == "MultiIndex": 

4771 if data_columns: 

4772 raise ValueError( 

4773 f"cannot use a multi-index on axis [{axis}] with " 

4774 f"data_columns {data_columns}" 

4775 ) 

4776 if isinstance(min_itemsize, dict): 

4777 # GH#12154 'values' sizes every string column and the index 

4778 # cname(s) size the row index; both are legal here. Only 

4779 # per-column keys are unsupported for MultiIndex columns. 

4780 allowed = {"values", *index_cnames} 

4781 mi_keys = [k for k in min_itemsize if k not in allowed] 

4782 if mi_keys: 

4783 raise ValueError( 

4784 f"cannot use min_itemsize keys {mi_keys} on axis " 

4785 f"[{axis}] with a MultiIndex; per-column " 

4786 "min_itemsize requires data_columns, which are not " 

4787 "supported with MultiIndex columns. Use " 

4788 "min_itemsize={'values': N} to apply a single " 

4789 "min_itemsize across all string columns." 

4790 ) 

4791 

4792 # evaluate the passed data_columns, True == use all columns 

4793 # take only valid axis labels 

4794 if data_columns is True: 

4795 data_columns = list(axis_labels) 

4796 elif data_columns is None: 

4797 data_columns = [] 

4798 

4799 # if min_itemsize is a dict, add the keys (exclude 'values') 

4800 if isinstance(min_itemsize, dict): 

4801 existing_data_columns = set(data_columns) 

4802 data_columns = list(data_columns) # ensure we do not modify 

4803 data_columns.extend( 

4804 [ 

4805 k 

4806 for k in min_itemsize.keys() 

4807 if k != "values" and k not in existing_data_columns 

4808 ] 

4809 ) 

4810 

4811 # return valid columns in the order of our axis 

4812 return [c for c in data_columns if c in axis_labels] 

4813 

4814 def _create_axes( 

4815 self, 

4816 axes, 

4817 obj: DataFrame, 

4818 validate: bool = True, 

4819 nan_rep=None, 

4820 data_columns=None, 

4821 min_itemsize=None, 

4822 ): 

4823 """ 

4824 Create and return the axes. 

4825 

4826 Parameters 

4827 ---------- 

4828 axes: list or None 

4829 The names or numbers of the axes to create. 

4830 obj : DataFrame 

4831 The object to create axes on. 

4832 validate: bool, default True 

4833 Whether to validate the obj against an existing object already written. 

4834 nan_rep : 

4835 A value to use for string column nan_rep. 

4836 data_columns : List[str], True, or None, default None 

4837 Specify the columns that we want to create to allow indexing on. 

4838 

4839 * True : Use all available columns. 

4840 * None : Use no columns. 

4841 * List[str] : Use the specified columns. 

4842 

4843 min_itemsize: Dict[str, int] or None, default None 

4844 The min itemsize for a column in bytes. 

4845 """ 

4846 if not isinstance(obj, DataFrame): 

4847 group = self.group._v_name 

4848 raise TypeError( 

4849 f"cannot properly create the storer for: [group->{group}," 

4850 f"value->{type(obj)}]" 

4851 ) 

4852 

4853 # set the default axes if needed 

4854 if axes is None: 

4855 axes = [0] 

4856 

4857 # map axes to numbers 

4858 axes = [obj._get_axis_number(a) for a in axes] 

4859 

4860 # GH#9604: None means no nan_rep was requested, so each string column 

4861 # mints a collision-free sentinel instead. A caller-supplied one keeps 

4862 # its documented table-wide meaning and is persisted, so a later append 

4863 # to the same table keeps it. 

4864 user_nan_rep = nan_rep 

4865 

4866 # do we have an existing table (if so, use its axes & data_columns) 

4867 if self.infer_axes(): 

4868 table_exists = True 

4869 axes = [a.axis for a in self.index_axes] 

4870 data_columns = list(self.data_columns) 

4871 # an existing table's nan_rep supersedes whatever this append passed 

4872 nan_rep = user_nan_rep = self.nan_rep 

4873 # TODO: do we always have validate=True here? 

4874 else: 

4875 table_exists = False 

4876 

4877 new_info = self.info 

4878 

4879 assert self.ndim == 2 # with next check, we must have len(axes) == 1 

4880 # currently support on ndim-1 axes 

4881 if len(axes) != self.ndim - 1: 

4882 raise ValueError( 

4883 "currently only support ndim-1 indexers in an AppendableTable" 

4884 ) 

4885 

4886 # create according to the new data 

4887 new_non_index_axes: list = [] 

4888 

4889 # nan_representation 

4890 if nan_rep is None: 

4891 nan_rep = "nan" 

4892 

4893 # We construct the non-index-axis first, since that alters new_info 

4894 idx = next(x for x in [0, 1] if x not in axes) 

4895 

4896 a = obj.axes[idx] 

4897 # we might be able to change the axes on the appending data if necessary 

4898 append_axis = list(a) 

4899 if table_exists: 

4900 indexer = len(new_non_index_axes) # i.e. 0 

4901 exist_axis = self.non_index_axes[indexer][1] 

4902 if not array_equivalent( 

4903 np.array(append_axis), 

4904 np.array(exist_axis), 

4905 strict_nan=True, 

4906 dtype_equal=True, 

4907 ): 

4908 # ahah! -> reindex 

4909 if array_equivalent( 

4910 np.array(sorted(append_axis)), 

4911 np.array(sorted(exist_axis)), 

4912 strict_nan=True, 

4913 dtype_equal=True, 

4914 ): 

4915 append_axis = exist_axis 

4916 

4917 # the non_index_axes info 

4918 info = new_info.setdefault(idx, {}) 

4919 info["names"] = list(a.names) 

4920 info["type"] = type(a).__name__ 

4921 

4922 new_non_index_axes.append((idx, append_axis)) 

4923 

4924 # Now we can construct our new index axis 

4925 idx = axes[0] 

4926 a = obj.axes[idx] 

4927 axis_name = obj._get_axis_name(idx) 

4928 # GH#9604: reuse the string-Index NaN sentinel persisted by an earlier 

4929 # append so it is stable across chunks; if none has been persisted yet 

4930 # but this chunk introduces a missing value, choose the sentinel against 

4931 # the values already stored so it cannot collide with them. 

4932 existing_nan_rep = None 

4933 existing_values = None 

4934 if table_exists: 

4935 existing_index_col = self.index_axes[0] 

4936 existing_nan_rep = existing_index_col.nan_rep 

4937 if existing_index_col.kind == "string" and not hasattr( 

4938 self.table.attrs, existing_index_col.nan_rep_attr 

4939 ): 

4940 # _read_index_nan_rep assumes "nan" is the sentinel of a file 

4941 # written before one was persisted, which only holds if such a 

4942 # value is stored. Without one the column has no sentinel to 

4943 # protect, so a literal "nan" may still be appended to it. Test 

4944 # the raw bytes rather than decoding a possibly large column. 

4945 stored = getattr(self.table.cols, existing_index_col.cname)[:] 

4946 if not (stored == existing_nan_rep.encode(self.encoding)).any(): 

4947 existing_nan_rep = None 

4948 if ( 

4949 existing_nan_rep is None 

4950 and existing_index_col.kind == "string" 

4951 and np.asarray(a.isna()).any() 

4952 ): 

4953 existing_values = _read_stored_column_values( 

4954 self.table, existing_index_col.cname, self.encoding, self.errors 

4955 ) 

4956 new_index = _convert_index( 

4957 axis_name, 

4958 a, 

4959 self.encoding, 

4960 self.errors, 

4961 existing_nan_rep=existing_nan_rep, 

4962 existing_values=existing_values, 

4963 ) 

4964 new_index.axis = idx 

4965 

4966 # Because we are always 2D, there is only one new_index, so 

4967 # we know it will have pos=0 

4968 new_index.set_pos(0) 

4969 

4970 if table_exists and new_index.kind == existing_index_col.kind == "integer": 

4971 # A PeriodIndex is stored as its i8 ordinals with kind "integer", so 

4972 # freq is the only thing separating it from a plain integer index. 

4973 # Check it before update_info silently overwrites the stored freq, 

4974 # which would reinterpret the rows already stored. GH#68523 

4975 existing_freq = new_info.get(new_index.name, {}).get("freq") 

4976 if existing_freq != new_index.freq: 

4977 if existing_freq is None: 

4978 raise TypeError( 

4979 "cannot append a period index to a non-period index" 

4980 ) 

4981 if new_index.freq is None: 

4982 raise TypeError( 

4983 "cannot append a non-period index to a period index" 

4984 ) 

4985 # PeriodDtype's alias ("M"), not the offset's datetime alias 

4986 # ("ME"), which Period itself rejects. Same alias and warning 

4987 # filter as raise_on_incompatible. 

4988 with warnings.catch_warnings(): 

4989 warnings.filterwarnings( 

4990 "ignore", 

4991 r"PeriodDtype\[B\] is deprecated", 

4992 category=FutureWarning, 

4993 ) 

4994 existing_str = PeriodDtype(existing_freq)._freqstr 

4995 new_str = PeriodDtype(new_index.freq)._freqstr 

4996 raise TypeError( 

4997 f"incompatible freq in col [{existing_str} - {new_str}]" 

4998 ) 

4999 

5000 # tz can only differ within a matching kind; a differing one already 

5001 # gets validate_attr's more specific message. 

5002 if table_exists and new_index.kind == existing_index_col.kind: 

5003 _check_tz_conflict( 

5004 new_index.name, 

5005 new_info.get(new_index.name, {}).get("tz"), 

5006 new_index.tz, 

5007 ) 

5008 

5009 new_index.update_info(new_info) 

5010 new_index.maybe_set_size(min_itemsize) # check for column conflicts 

5011 

5012 new_index_axes = [new_index] 

5013 j = len(new_index_axes) # i.e. 1 

5014 assert j == 1 

5015 

5016 # reindex by our non_index_axes & compute data_columns 

5017 assert len(new_non_index_axes) == 1 

5018 for a in new_non_index_axes: 

5019 obj = _reindex_axis(obj, a[0], a[1]) 

5020 

5021 transposed = new_index.axis == 1 

5022 

5023 # figure out data_columns and get out blocks 

5024 data_columns = self.validate_data_columns( 

5025 data_columns, min_itemsize, new_non_index_axes, (new_index.cname,) 

5026 ) 

5027 

5028 if new_index.cname in data_columns: 

5029 # GH#41437 the implicit row index is stored under the reserved 

5030 # cname (typically 'index') and a data column of the same name 

5031 # would collide with it in the table description. 

5032 raise ValueError( 

5033 f"cannot use a column named {new_index.cname!r} as a " 

5034 f"data_column; {new_index.cname!r} is reserved for the " 

5035 "implicit row index" 

5036 ) 

5037 

5038 frame = self.get_object(obj, transposed)._consolidate() 

5039 

5040 blocks, blk_items = self._get_blocks_and_items( 

5041 frame, table_exists, new_non_index_axes, self.values_axes, data_columns 

5042 ) 

5043 

5044 # add my values 

5045 vaxes = [] 

5046 for i, (blk, b_items) in enumerate(zip(blocks, blk_items, strict=True)): 

5047 # shape of the data column are the indexable axes 

5048 klass = DataCol 

5049 name = None 

5050 

5051 # we have a data_column 

5052 if data_columns and len(b_items) == 1 and b_items[0] in data_columns: 

5053 klass = DataIndexableCol 

5054 name = b_items[0] 

5055 if not (name is None or isinstance(name, str)): 

5056 # TODO: should the message here be more specifically non-str? 

5057 raise ValueError("cannot have non-object label DataIndexableCol") 

5058 

5059 # make sure that we match up the existing columns 

5060 # if we have an existing table 

5061 existing_col: DataCol | None 

5062 

5063 if table_exists and validate: 

5064 try: 

5065 existing_col = self.values_axes[i] 

5066 except (IndexError, KeyError) as err: 

5067 raise ValueError( 

5068 f"Incompatible appended table [{blocks}]" 

5069 f"with existing table [{self.values_axes}]" 

5070 ) from err 

5071 else: 

5072 existing_col = None 

5073 

5074 new_name = name or f"values_block_{i}" 

5075 

5076 # GH#9604: the read path honors a per-column sentinel only when 

5077 # the table carries the marker attribute, which set_attrs writes at 

5078 # creation and an append cannot add retroactively. So when appending 

5079 # to a table written before the marker existed, keep encoding 

5080 # missing values with the table-wide nan_rep, otherwise the sentinel 

5081 # would be read back as a literal string. 

5082 level_names = self.levels if isinstance(self.levels, list) else [] 

5083 is_level = name is not None and name in level_names 

5084 if table_exists: 

5085 uses_col_nan_rep = bool( 

5086 getattr(self.attrs, "data_col_nan_rep", False) 

5087 ) or (is_level and bool(getattr(self.attrs, "mi_level_nan_rep", False))) 

5088 else: 

5089 # a MultiIndex level is part of the index rather than of the 

5090 # data, so a caller-supplied nan_rep does not govern it 

5091 uses_col_nan_rep = user_nan_rep is None or is_level 

5092 col_nan_rep = None 

5093 if uses_col_nan_rep: 

5094 col_nan_rep = _make_data_col_nan_rep( 

5095 blk.values, 

5096 b_items, 

5097 existing_col, 

5098 self.table, 

5099 self.encoding, 

5100 self.errors, 

5101 ) 

5102 block_nan_rep = col_nan_rep if col_nan_rep is not None else nan_rep 

5103 

5104 data_converted = _maybe_convert_for_string_atom( 

5105 new_name, 

5106 blk.values, 

5107 existing_col=existing_col, 

5108 min_itemsize=min_itemsize, 

5109 nan_rep=block_nan_rep, 

5110 encoding=self.encoding, 

5111 errors=self.errors, 

5112 columns=b_items, 

5113 ) 

5114 

5115 typ = klass._get_atom(data_converted) 

5116 kind = _dtype_to_kind(data_converted.dtype.name) 

5117 tz = None 

5118 if getattr(data_converted, "tz", None) is not None: 

5119 tz = _get_tz(data_converted.tz) 

5120 

5121 meta = metadata = ordered = None 

5122 if isinstance(data_converted.dtype, CategoricalDtype): 

5123 ordered = data_converted.ordered 

5124 meta = "category" 

5125 metadata = np.asarray(data_converted.categories).ravel() 

5126 elif isinstance(blk.dtype, StringDtype): 

5127 meta = str(blk.dtype) 

5128 

5129 data, dtype_name = _get_data_and_dtype_name(data_converted) 

5130 

5131 # A new tz-aware data column's kind carries the tz while the 

5132 # stored one's does not, so gate on the dtype actually persisted. 

5133 if existing_col is not None and existing_col.dtype == dtype_name: 

5134 _check_tz_conflict(new_name, new_info.get(new_name, {}).get("tz"), tz) 

5135 

5136 col = klass( 

5137 name=new_name, 

5138 cname=new_name, 

5139 values=list(b_items), 

5140 typ=typ, 

5141 pos=j, 

5142 kind=kind, 

5143 tz=tz, 

5144 ordered=ordered, 

5145 meta=meta, 

5146 metadata=metadata, 

5147 dtype=dtype_name, 

5148 data=data, 

5149 ) 

5150 col.uses_col_nan_rep = uses_col_nan_rep 

5151 col.nan_rep = col_nan_rep 

5152 col.update_info(new_info) 

5153 

5154 vaxes.append(col) 

5155 

5156 j += 1 

5157 

5158 dcs = [col.name for col in vaxes if col.is_data_indexable] 

5159 

5160 new_table = type(self)( 

5161 parent=self.parent, 

5162 group=self.group, 

5163 encoding=self.encoding, 

5164 errors=self.errors, 

5165 index_axes=new_index_axes, 

5166 non_index_axes=new_non_index_axes, 

5167 values_axes=vaxes, 

5168 data_columns=dcs, 

5169 info=new_info, 

5170 nan_rep=user_nan_rep, 

5171 ) 

5172 if hasattr(self, "levels"): 

5173 # TODO: get this into constructor, only for appropriate subclass 

5174 new_table.levels = self.levels 

5175 

5176 new_table.validate_min_itemsize(min_itemsize) 

5177 

5178 if validate and table_exists: 

5179 new_table.validate(self) 

5180 

5181 return new_table 

5182 

5183 @staticmethod 

5184 def _get_blocks_and_items( 

5185 frame: DataFrame, 

5186 table_exists: bool, 

5187 new_non_index_axes, 

5188 values_axes, 

5189 data_columns, 

5190 ): 

5191 # Helper to clarify non-state-altering parts of _create_axes 

5192 def get_blk_items(mgr): 

5193 return [mgr.items.take(blk.mgr_locs) for blk in mgr.blocks] 

5194 

5195 mgr = frame._mgr 

5196 blocks: list[Block] = list(mgr.blocks) 

5197 blk_items: list[Index] = get_blk_items(mgr) 

5198 

5199 if len(data_columns): 

5200 # TODO: prove that we only get here with axis == 1? 

5201 # It is the case in all extant tests, but NOT the case 

5202 # outside this `if len(data_columns)` check. 

5203 

5204 axis, axis_labels = new_non_index_axes[0] 

5205 new_labels = Index(axis_labels).difference(Index(data_columns)) 

5206 mgr = frame.reindex(new_labels, axis=axis)._mgr 

5207 

5208 blocks = list(mgr.blocks) 

5209 blk_items = get_blk_items(mgr) 

5210 for c in data_columns: 

5211 # This reindex would raise ValueError if we had a duplicate 

5212 # index, so we can infer that (as long as axis==1) we 

5213 # get a single column back, so a single block. 

5214 mgr = frame.reindex([c], axis=axis)._mgr 

5215 blocks.extend(mgr.blocks) 

5216 blk_items.extend(get_blk_items(mgr)) 

5217 

5218 # reorder the blocks in the same order as the existing table if we can 

5219 if table_exists: 

5220 by_items = { 

5221 tuple(b_items.tolist()): (b, b_items) 

5222 for b, b_items in zip(blocks, blk_items, strict=True) 

5223 } 

5224 new_blocks: list[Block] = [] 

5225 new_blk_items = [] 

5226 for ea in values_axes: 

5227 items = tuple(ea.values) 

5228 try: 

5229 b, b_items = by_items.pop(items) 

5230 new_blocks.append(b) 

5231 new_blk_items.append(b_items) 

5232 except (IndexError, KeyError) as err: 

5233 jitems = ",".join([pprint_thing(item) for item in items]) 

5234 raise ValueError( 

5235 f"cannot match existing table structure for [{jitems}] " 

5236 "on appending data" 

5237 ) from err 

5238 blocks = new_blocks 

5239 blk_items = new_blk_items 

5240 

5241 return blocks, blk_items 

5242 

5243 def process_axes(self, obj, selection: Selection, columns=None) -> DataFrame: 

5244 """process axes filters""" 

5245 # make a copy to avoid side effects 

5246 if columns is not None: 

5247 columns = list(columns) 

5248 

5249 # make sure to include levels if we have them 

5250 if columns is not None and self.is_multi_index: 

5251 assert isinstance(self.levels, list) # assured by is_multi_index 

5252 for n in self.levels: 

5253 if n not in columns: 

5254 columns.insert(0, n) 

5255 

5256 # reorder by any non_index_axes & limit to the select columns 

5257 for axis, labels in self.non_index_axes: 

5258 obj = _reindex_axis(obj, axis, labels, columns) 

5259 

5260 def process_filter(field, filt, op): 

5261 for axis_name in obj._AXIS_ORDERS: 

5262 axis_number = obj._get_axis_number(axis_name) 

5263 axis_values = obj._get_axis(axis_name) 

5264 assert axis_number is not None 

5265 

5266 # see if the field is the name of an axis 

5267 if field == axis_name: 

5268 # if we have a multi-index, then need to include 

5269 # the levels 

5270 if self.is_multi_index: 

5271 filt = filt.union(Index(self.levels)) 

5272 

5273 takers = op(axis_values, filt) 

5274 return obj.loc(axis=axis_number)[takers] 

5275 

5276 # this might be the name of a file IN an axis 

5277 elif field in axis_values: 

5278 # we need to filter on this dimension 

5279 # use the column Series rather than .values so the 

5280 # filter respects the column dtype -- .values strips 

5281 # tz from a datetimetz column and the isin below then 

5282 # matches nothing (GH#12953). This mirrors the 

5283 # coordinate path in Selection.select_coords. 

5284 values = obj[field] 

5285 filt = ensure_index(filt) 

5286 

5287 # hack until we support reversed dim flags 

5288 if isinstance(obj, DataFrame): 

5289 axis_number = 1 - axis_number 

5290 

5291 takers = op(values, filt) 

5292 return obj.loc(axis=axis_number)[takers] 

5293 

5294 raise ValueError(f"cannot find the field [{field}] for filtering!") 

5295 

5296 # apply the selection filters (but keep in the same order) 

5297 if selection.filter is not None: 

5298 for field, op, filt in selection.filter.format(): 

5299 obj = process_filter(field, filt, op) 

5300 

5301 return obj 

5302 

5303 def create_description( 

5304 self, 

5305 complib, 

5306 complevel: int | None, 

5307 fletcher32: bool, 

5308 expectedrows: int | None, 

5309 ) -> dict[str, Any]: 

5310 """create the description of the table from the axes & values""" 

5311 # provided expected rows if its passed 

5312 if expectedrows is None: 

5313 expectedrows = max(self.nrows_expected, 10000) 

5314 

5315 d = {"name": "table", "expectedrows": expectedrows} 

5316 

5317 # description from the axes & values 

5318 d["description"] = {a.cname: a.typ for a in self.axes} 

5319 

5320 if complib: 

5321 if complevel is None: 

5322 complevel = self._complevel or 9 

5323 filters = _tables().Filters( 

5324 complevel=complevel, 

5325 complib=complib, 

5326 fletcher32=fletcher32 or self._fletcher32, 

5327 ) 

5328 d["filters"] = filters 

5329 elif self._filters is not None: 

5330 d["filters"] = self._filters 

5331 

5332 return d 

5333 

5334 def read_coordinates( 

5335 self, where=None, start: int | None = None, stop: int | None = None 

5336 ): 

5337 """ 

5338 select coordinates (row numbers) from a table; return the 

5339 coordinates object 

5340 """ 

5341 # infer the data kind 

5342 if not self.infer_axes(): 

5343 return False 

5344 

5345 if where is not None and self._where_selects_columns(where): 

5346 raise NotImplementedError( 

5347 "selecting columns through the 'where' expression " 

5348 "(e.g. \"columns=['A']\") is a column projection, not a row " 

5349 "filter, so it cannot be read as row coordinates (e.g. with " 

5350 "select_as_coordinates, select_as_multiple, iterator, or " 

5351 "chunksize); use the 'columns' argument of 'select' instead" 

5352 ) 

5353 

5354 # create the selection 

5355 selection = Selection(self, where=where, start=start, stop=stop) 

5356 coords = selection.select_coords() 

5357 

5358 return Index(coords, copy=False) 

5359 

5360 def _where_selects_columns(self, where) -> bool: 

5361 """ 

5362 Whether ``where`` contains a column selection such as "columns=['A']". 

5363 

5364 Such a selection is a column projection rather than a row filter, which 

5365 cannot be applied via row coordinates and so is unsupported on any 

5366 coordinate-based read -- ``iterator``/``chunksize``, 

5367 :meth:`select_as_coordinates`, and :meth:`select_as_multiple` 

5368 (GH#12953); ``read_coordinates`` raises a clear error instead of a 

5369 cryptic ``KeyError``. 

5370 """ 

5371 if not self.non_index_axes: 

5372 return False 

5373 

5374 selection = Selection(self, where=where) 

5375 if selection.filter is None: 

5376 return False 

5377 

5378 # a data-column row filter has a field matching a readable column; a 

5379 # column selection's field is the column-axis name (e.g. "columns") 

5380 data_column_names = {a.name for a in self.axes} 

5381 return any( 

5382 field not in data_column_names 

5383 for field, _op, _filt in selection.filter.format() 

5384 ) 

5385 

5386 def read_column( 

5387 self, 

5388 column: str, 

5389 where=None, 

5390 start: int | None = None, 

5391 stop: int | None = None, 

5392 ): 

5393 """ 

5394 return a single column from the table, generally only indexables 

5395 are interesting 

5396 """ 

5397 # infer the data kind 

5398 if not self.infer_axes(): 

5399 return False 

5400 

5401 if where is not None: 

5402 raise TypeError("read_column does not currently accept a where clause") 

5403 

5404 # find the axes 

5405 for a in self.axes: 

5406 if column == a.name: 

5407 if not a.is_data_indexable: 

5408 raise ValueError( 

5409 f"column [{column}] can not be extracted individually; " 

5410 "it is not data indexable" 

5411 ) 

5412 

5413 # column must be an indexable or a data column 

5414 c = getattr(self.table.cols, column) 

5415 a.set_info(self.info) 

5416 col_values = a.convert( 

5417 c[start:stop], 

5418 nan_rep=self.nan_rep, 

5419 encoding=self.encoding, 

5420 errors=self.errors, 

5421 ) 

5422 cvs = col_values[1] 

5423 dtype = getattr(self.table.attrs, f"{column}_meta", None) 

5424 return Series(cvs, name=column, copy=False, dtype=dtype) 

5425 

5426 raise KeyError(f"column [{column}] not found in the table") 

5427 

5428 

5429class WORMTable(Table): 

5430 """ 

5431 a write-once read-many table: this format DOES NOT ALLOW appending to a 

5432 table. writing is a one-time operation the data are stored in a format 

5433 that allows for searching the data on disk 

5434 """ 

5435 

5436 table_type = "worm" 

5437 

5438 def read( 

5439 self, 

5440 where=None, 

5441 columns=None, 

5442 start: int | None = None, 

5443 stop: int | None = None, 

5444 ): 

5445 """ 

5446 read the indices and the indexing array, calculate offset rows and return 

5447 """ 

5448 raise NotImplementedError("WORMTable needs to implement read") 

5449 

5450 def write(self, obj, **kwargs) -> None: 

5451 """ 

5452 write in a format that we can search later on (but cannot append 

5453 to): write out the indices and the values using _write_array 

5454 (e.g. a CArray) create an indexing table so that we can search 

5455 """ 

5456 raise NotImplementedError("WORMTable needs to implement write") 

5457 

5458 

5459class AppendableTable(Table): 

5460 """support the new appendable table formats""" 

5461 

5462 table_type = "appendable" 

5463 

5464 # error: Signature of "write" incompatible with supertype "Fixed" 

5465 def write( # type: ignore[override] 

5466 self, 

5467 obj, 

5468 axes=None, 

5469 append: bool = False, 

5470 complib=None, 

5471 complevel=None, 

5472 fletcher32=None, 

5473 min_itemsize=None, 

5474 chunksize: int | None = None, 

5475 expectedrows=None, 

5476 dropna: bool = False, 

5477 nan_rep=None, 

5478 data_columns=None, 

5479 track_times: bool = True, 

5480 ) -> None: 

5481 if not append and self.is_exists: 

5482 self._handle.remove_node(self.group, "table") 

5483 

5484 # create the axes 

5485 table = self._create_axes( 

5486 axes=axes, 

5487 obj=obj, 

5488 validate=append, 

5489 min_itemsize=min_itemsize, 

5490 nan_rep=nan_rep, 

5491 data_columns=data_columns, 

5492 ) 

5493 

5494 for a in table.axes: 

5495 a.validate_names() 

5496 

5497 if not table.is_exists: 

5498 # create the table 

5499 options = table.create_description( 

5500 complib=complib, 

5501 complevel=complevel, 

5502 fletcher32=fletcher32, 

5503 expectedrows=expectedrows, 

5504 ) 

5505 

5506 # set the table attributes 

5507 table.set_attrs() 

5508 

5509 options["track_times"] = track_times 

5510 

5511 # create the table 

5512 table._handle.create_table(table.group, **options) 

5513 

5514 # update my info 

5515 table.attrs.info = table.info 

5516 

5517 # validate the axes and set the kinds 

5518 for a in table.axes: 

5519 a.validate_and_set(table, append) 

5520 

5521 # add the rows 

5522 table.write_data(chunksize, dropna=dropna) 

5523 

5524 def write_data(self, chunksize: int | None, dropna: bool = False) -> None: 

5525 """ 

5526 we form the data into a 2-d including indexes,values,mask write chunk-by-chunk 

5527 """ 

5528 names = self.dtype.names 

5529 nrows = self.nrows_expected 

5530 

5531 # if dropna==True, then drop ALL nan rows 

5532 masks = [] 

5533 if dropna: 

5534 for a in self.values_axes: 

5535 # figure the mask: only do if we can successfully process this 

5536 # column, otherwise ignore the mask 

5537 mask = isna(a.data).all(axis=0) 

5538 if isinstance(mask, np.ndarray): 

5539 masks.append(mask.astype("u1", copy=False)) 

5540 

5541 # consolidate masks 

5542 if masks: 

5543 mask = masks[0] 

5544 for m in masks[1:]: 

5545 mask = mask & m 

5546 mask = mask.ravel() 

5547 else: 

5548 mask = None 

5549 

5550 # broadcast the indexes if needed 

5551 indexes = [a.cvalues for a in self.index_axes] 

5552 nindexes = len(indexes) 

5553 assert nindexes == 1, nindexes # ensures we dont need to broadcast 

5554 

5555 # transpose the values so first dimension is last 

5556 # reshape the values if needed 

5557 values = [a.take_data() for a in self.values_axes] 

5558 values = [v.transpose(np.roll(np.arange(v.ndim), v.ndim - 1)) for v in values] 

5559 bvalues = [] 

5560 for i, v in enumerate(values): 

5561 new_shape = (nrows, *self.dtype[names[nindexes + i]].shape) 

5562 bvalues.append(v.reshape(new_shape)) 

5563 

5564 # write the chunks 

5565 if chunksize is None: 

5566 chunksize = 100000 

5567 

5568 rows = np.empty(min(chunksize, nrows), dtype=self.dtype) 

5569 chunks = nrows // chunksize + 1 

5570 for i in range(chunks): 

5571 start_i = i * chunksize 

5572 end_i = min((i + 1) * chunksize, nrows) 

5573 if start_i >= end_i: 

5574 break 

5575 

5576 self.write_data_chunk( 

5577 rows, 

5578 indexes=[a[start_i:end_i] for a in indexes], # pyright: ignore[reportOptionalSubscript] 

5579 mask=mask[start_i:end_i] if mask is not None else None, 

5580 values=[v[start_i:end_i] for v in bvalues], 

5581 ) 

5582 

5583 def write_data_chunk( 

5584 self, 

5585 rows: np.ndarray, 

5586 indexes: list[np.ndarray], 

5587 mask: npt.NDArray[np.bool_] | None, 

5588 values: list[np.ndarray], 

5589 ) -> None: 

5590 """ 

5591 Parameters 

5592 ---------- 

5593 rows : an empty memory space where we are putting the chunk 

5594 indexes : an array of the indexes 

5595 mask : an array of the masks 

5596 values : an array of the values 

5597 """ 

5598 # 0 len 

5599 for v in values: 

5600 if not np.prod(v.shape): 

5601 return 

5602 

5603 nrows = indexes[0].shape[0] 

5604 if nrows != len(rows): 

5605 rows = np.empty(nrows, dtype=self.dtype) 

5606 names = self.dtype.names 

5607 nindexes = len(indexes) 

5608 

5609 # indexes 

5610 for i, idx in enumerate(indexes): 

5611 rows[names[i]] = idx 

5612 

5613 # values 

5614 for i, v in enumerate(values): 

5615 rows[names[i + nindexes]] = v 

5616 

5617 # mask 

5618 if mask is not None: 

5619 m = ~mask.ravel().astype(bool, copy=False) 

5620 if not m.all(): 

5621 rows = rows[m] 

5622 

5623 if len(rows): 

5624 self.table.append(rows) 

5625 self.table.flush() 

5626 

5627 def delete( 

5628 self, where=None, start: int | None = None, stop: int | None = None 

5629 ) -> int | None: 

5630 # delete all rows (and return the nrows) 

5631 if where is None or not len(where): 

5632 if start is None and stop is None: 

5633 nrows = self.nrows 

5634 self._handle.remove_node(self.group, recursive=True) 

5635 else: 

5636 # pytables<3.0 would remove a single row with stop=None 

5637 if stop is None: 

5638 stop = self.nrows 

5639 nrows = self.table.remove_rows(start=start, stop=stop) 

5640 self.table.flush() 

5641 return nrows 

5642 

5643 # infer the data kind 

5644 if not self.infer_axes(): 

5645 return None 

5646 

5647 # create the selection 

5648 table = self.table 

5649 selection = Selection(self, where, start=start, stop=stop) 

5650 values = selection.select_coords() 

5651 

5652 # delete the rows in reverse order 

5653 sorted_series = Series(values, copy=False).sort_values() 

5654 ln = len(sorted_series) 

5655 

5656 if ln: 

5657 # construct groups of consecutive rows 

5658 diff = sorted_series.diff() 

5659 groups = list(diff[diff > 1].index) 

5660 

5661 # 1 group 

5662 if not groups: 

5663 groups = [0] 

5664 

5665 # final element 

5666 if groups[-1] != ln: 

5667 groups.append(ln) 

5668 

5669 # initial element 

5670 if groups[0] != 0: 

5671 groups.insert(0, 0) 

5672 

5673 # we must remove in reverse order! 

5674 pg = groups.pop() 

5675 for g in reversed(groups): 

5676 rows = sorted_series.take(range(g, pg)) 

5677 table.remove_rows( 

5678 start=rows[rows.index[0]], stop=rows[rows.index[-1]] + 1 

5679 ) 

5680 pg = g 

5681 

5682 self.table.flush() 

5683 

5684 # return the number of rows removed 

5685 return ln 

5686 

5687 

5688class AppendableFrameTable(AppendableTable): 

5689 """support the new appendable table formats""" 

5690 

5691 pandas_kind = "frame_table" 

5692 table_type = "appendable_frame" 

5693 ndim = 2 

5694 obj_type: type[DataFrame | Series] = DataFrame 

5695 

5696 @property 

5697 def is_transposed(self) -> bool: 

5698 return self.index_axes[0].axis == 1 

5699 

5700 @classmethod 

5701 def get_object(cls, obj, transposed: bool): 

5702 """these are written transposed""" 

5703 if transposed: 

5704 obj = obj.T 

5705 return obj 

5706 

5707 def read( 

5708 self, 

5709 where=None, 

5710 columns=None, 

5711 start: int | None = None, 

5712 stop: int | None = None, 

5713 ): 

5714 # infer the data kind 

5715 if not self.infer_axes(): 

5716 return None 

5717 

5718 result = self._read_axes(where=where, start=start, stop=stop) 

5719 

5720 info = ( 

5721 self.info.get(self.non_index_axes[0][0], {}) 

5722 if len(self.non_index_axes) 

5723 else {} 

5724 ) 

5725 

5726 inds = [i for i, ax in enumerate(self.axes) if ax is self.index_axes[0]] 

5727 assert len(inds) == 1 

5728 ind = inds[0] 

5729 

5730 index = result[ind][0] 

5731 

5732 frames = [] 

5733 for i, a in enumerate(self.axes): 

5734 if a not in self.values_axes: 

5735 continue 

5736 index_vals, cvalues = result[i] 

5737 

5738 # we could have a multi-index constructor here 

5739 # ensure_index doesn't recognized our list-of-tuples here 

5740 if info.get("type") != "MultiIndex": 

5741 cols = Index(index_vals) 

5742 else: 

5743 cols = MultiIndex.from_tuples(index_vals) 

5744 

5745 names = info.get("names") 

5746 if names is not None: 

5747 cols.set_names(names, inplace=True) 

5748 

5749 if self.is_transposed: 

5750 values = cvalues 

5751 index_ = cols 

5752 cols_ = Index(index, name=getattr(index, "name", None)) 

5753 else: 

5754 values = cvalues.T 

5755 index_ = Index(index, name=getattr(index, "name", None)) 

5756 cols_ = cols 

5757 

5758 # if we have a DataIndexableCol, its shape will only be 1 dim 

5759 if values.ndim == 1 and isinstance(values, np.ndarray): 

5760 values = values.reshape((1, values.shape[0])) 

5761 

5762 if isinstance(values, (np.ndarray, DatetimeArray)): 

5763 try: 

5764 df = DataFrame(values.T, columns=cols_, index=index_, copy=False) 

5765 except UnicodeEncodeError as err: 

5766 if ( 

5767 self.errors == "surrogatepass" 

5768 and using_string_dtype() 

5769 and str(err).endswith("surrogates not allowed") 

5770 and HAS_PYARROW 

5771 ): 

5772 df = DataFrame( 

5773 values.T, 

5774 columns=cols_, 

5775 index=index_, 

5776 copy=False, 

5777 dtype=StringDtype(storage="python", na_value=np.nan), 

5778 ) 

5779 else: 

5780 raise 

5781 elif isinstance(values, Index): 

5782 df = DataFrame(values, columns=cols_, index=index_) 

5783 else: 

5784 # Categorical 

5785 df = DataFrame._from_arrays([values], columns=cols_, index=index_) 

5786 if not (using_string_dtype() and values.dtype.kind == "O"): 

5787 assert (df.dtypes == values.dtype).all(), (df.dtypes, values.dtype) 

5788 

5789 # If str / string dtype is stored in meta, use that. 

5790 for column in cols_: 

5791 dtype = getattr(self.table.attrs, f"{column}_meta", None) 

5792 if dtype in ["str", "string"]: 

5793 df[column] = df[column].astype(dtype) 

5794 frames.append(df) 

5795 

5796 if len(frames) == 1: 

5797 df = frames[0] 

5798 else: 

5799 df = concat(frames, axis=1) 

5800 

5801 selection = Selection(self, where=where, start=start, stop=stop) 

5802 # apply the selection filters & axis orderings 

5803 df = self.process_axes(df, selection=selection, columns=columns) 

5804 return df 

5805 

5806 

5807class AppendableSeriesTable(AppendableFrameTable): 

5808 """support the new appendable table formats""" 

5809 

5810 pandas_kind = "series_table" 

5811 table_type = "appendable_series" 

5812 ndim = 2 

5813 obj_type = Series 

5814 

5815 @property 

5816 def is_transposed(self) -> bool: 

5817 return False 

5818 

5819 @classmethod 

5820 def get_object(cls, obj, transposed: bool): 

5821 return obj 

5822 

5823 # error: Signature of "write" incompatible with supertype "Fixed" 

5824 def write(self, obj, data_columns=None, **kwargs) -> None: # type: ignore[override] 

5825 """we are going to write this as a frame table""" 

5826 if not isinstance(obj, DataFrame): 

5827 name = obj.name or "values" 

5828 obj = obj.to_frame(name) 

5829 super().write(obj=obj, data_columns=obj.columns.tolist(), **kwargs) 

5830 

5831 def read( 

5832 self, 

5833 where=None, 

5834 columns=None, 

5835 start: int | None = None, 

5836 stop: int | None = None, 

5837 ) -> Series: 

5838 is_multi_index = self.is_multi_index 

5839 if columns is not None and is_multi_index: 

5840 assert isinstance(self.levels, list) # needed for mypy 

5841 for n in self.levels: 

5842 if n not in columns: 

5843 columns.insert(0, n) 

5844 s = super().read(where=where, columns=columns, start=start, stop=stop) 

5845 if is_multi_index: 

5846 s = s.set_index(self.levels) 

5847 

5848 s = s.iloc[:, 0] 

5849 

5850 # remove the default name 

5851 if s.name == "values": 

5852 s.name = None 

5853 return s 

5854 

5855 

5856class AppendableMultiSeriesTable(AppendableSeriesTable): 

5857 """support the new appendable table formats""" 

5858 

5859 pandas_kind = "series_table" 

5860 table_type = "appendable_multiseries" 

5861 

5862 # error: Signature of "write" incompatible with supertype "Fixed" 

5863 def write(self, obj, **kwargs) -> None: # type: ignore[override] 

5864 """we are going to write this as a frame table""" 

5865 name = obj.name or "values" 

5866 newobj, self.levels = self.validate_multiindex(obj) 

5867 assert isinstance(self.levels, list) # for mypy 

5868 cols = list(self.levels) 

5869 cols.append(name) 

5870 newobj.columns = Index(cols) 

5871 super().write(obj=newobj, **kwargs) 

5872 

5873 

5874class GenericTable(AppendableFrameTable): 

5875 """a table that read/writes the generic pytables table format""" 

5876 

5877 pandas_kind = "frame_table" 

5878 table_type = "generic_table" 

5879 ndim = 2 

5880 obj_type = DataFrame 

5881 levels: list[Hashable] 

5882 

5883 @property 

5884 def pandas_type(self) -> str: 

5885 return self.pandas_kind 

5886 

5887 @property 

5888 def storable(self): 

5889 return getattr(self.group, "table", None) or self.group 

5890 

5891 def get_attrs(self) -> None: 

5892 """retrieve our attributes""" 

5893 self.non_index_axes = [] 

5894 self.nan_rep = None 

5895 self.levels = [] 

5896 

5897 self.index_axes = [a for a in self.indexables if a.is_an_indexable] 

5898 self.values_axes = [a for a in self.indexables if not a.is_an_indexable] 

5899 self.data_columns = [a.name for a in self.values_axes] 

5900 

5901 @cache_readonly 

5902 def indexables(self): 

5903 """create the indexables from the table description""" 

5904 d = self.description 

5905 

5906 # TODO: can we get a typ for this? AFAICT it is the only place 

5907 # where we aren't passing one 

5908 # the index columns is just a simple index 

5909 md = self.read_metadata("index") 

5910 meta = "category" if md is not None else None 

5911 index_col = GenericIndexCol( 

5912 name="index", axis=0, table=self.table, meta=meta, metadata=md 

5913 ) 

5914 

5915 _indexables: list[GenericIndexCol | GenericDataIndexableCol] = [index_col] 

5916 

5917 for i, n in enumerate(d._v_names): 

5918 assert isinstance(n, str) 

5919 

5920 atom = getattr(d, n) 

5921 md = self.read_metadata(n) 

5922 meta = "category" if md is not None else None 

5923 dc = GenericDataIndexableCol( 

5924 name=n, 

5925 pos=i, 

5926 values=[n], 

5927 typ=atom, 

5928 table=self.table, 

5929 meta=meta, 

5930 metadata=md, 

5931 ) 

5932 _indexables.append(dc) 

5933 

5934 return _indexables 

5935 

5936 # error: Signature of "write" incompatible with supertype "AppendableTable" 

5937 def write(self, **kwargs) -> None: # type: ignore[override] 

5938 raise NotImplementedError("cannot write on a generic table") 

5939 

5940 

5941class AppendableMultiFrameTable(AppendableFrameTable): 

5942 """a frame with a multi-index""" 

5943 

5944 table_type = "appendable_multiframe" 

5945 obj_type = DataFrame 

5946 ndim = 2 

5947 _re_levels = re.compile(r"^level_\d+$") 

5948 

5949 @property 

5950 def table_type_short(self) -> str: 

5951 return "appendable_multi" 

5952 

5953 # error: Signature of "write" incompatible with supertype "Fixed" 

5954 def write(self, obj, data_columns=None, **kwargs) -> None: # type: ignore[override] 

5955 if data_columns is None: 

5956 data_columns = [] 

5957 elif data_columns is True: 

5958 data_columns = obj.columns.tolist() 

5959 obj, self.levels = self.validate_multiindex(obj) 

5960 assert isinstance(self.levels, list) # for mypy 

5961 for n in self.levels: 

5962 if n not in data_columns: 

5963 data_columns.insert(0, n) 

5964 super().write(obj=obj, data_columns=data_columns, **kwargs) 

5965 

5966 def read( 

5967 self, 

5968 where=None, 

5969 columns=None, 

5970 start: int | None = None, 

5971 stop: int | None = None, 

5972 ) -> DataFrame: 

5973 df = super().read(where=where, columns=columns, start=start, stop=stop) 

5974 df = df.set_index(self.levels) 

5975 

5976 # remove names for 'level_%d' 

5977 df.index = df.index.set_names( 

5978 [None if self._re_levels.search(name) else name for name in df.index.names] 

5979 ) 

5980 

5981 return df 

5982 

5983 

5984def _reindex_axis( 

5985 obj: DataFrame, axis: AxisInt, labels: Index, other=None 

5986) -> DataFrame: 

5987 ax = obj._get_axis(axis) 

5988 labels = ensure_index(labels) 

5989 

5990 # try not to reindex even if other is provided 

5991 # if it equals our current index 

5992 if other is not None: 

5993 other = ensure_index(other) 

5994 if (other is None or labels.equals(other)) and labels.equals(ax): 

5995 return obj 

5996 

5997 labels = ensure_index(labels.unique()) 

5998 if other is not None: 

5999 labels = ensure_index(other.unique()).intersection(labels, sort=False) 

6000 if not labels.equals(ax): 

6001 slicer: list[slice | Index] = [slice(None, None)] * obj.ndim 

6002 slicer[axis] = labels 

6003 obj = obj.loc[tuple(slicer)] 

6004 return obj 

6005 

6006 

6007# tz to/from coercion 

6008 

6009 

6010def _get_tz(tz: tzinfo) -> str | tzinfo: 

6011 """for a tz-aware type, return an encoded zone""" 

6012 zone = timezones.get_timezone(tz) 

6013 return zone 

6014 

6015 

6016def _check_tz_conflict( 

6017 name: str, existing_tz: str | tzinfo | None, tz: str | tzinfo | None 

6018) -> None: 

6019 """ 

6020 Raise if an append would add or drop the timezone of a stored column. 

6021 

6022 The read path localizes the whole stored column with this single tz, so 

6023 changing it reinterprets the rows already written. update_info raises only 

6024 when both sides are set, leaving the None cases silent. GH#68583 

6025 """ 

6026 if existing_tz is None and tz is not None: 

6027 raise TypeError(f"cannot append tz-aware data to tz-naive col [{name}]") 

6028 if existing_tz is not None and tz is None: 

6029 raise TypeError(f"cannot append tz-naive data to tz-aware col [{name}]") 

6030 

6031 

6032def _set_tz( 

6033 values: npt.NDArray[np.int64], tz: str | tzinfo | None, datetime64_dtype: str 

6034) -> DatetimeArray: 

6035 """ 

6036 Coerce the values to a DatetimeArray with appropriate tz. 

6037 

6038 Parameters 

6039 ---------- 

6040 values : ndarray[int64] 

6041 tz : str, tzinfo, or None 

6042 datetime64_dtype : str, e.g. "datetime64[ns]", "datetime64[25s]" 

6043 """ 

6044 assert values.dtype == "i8", values.dtype 

6045 # Argument "tz" to "tz_to_dtype" has incompatible type "str | tzinfo | None"; 

6046 # expected "tzinfo" 

6047 unit, _ = np.datetime_data(datetime64_dtype) # parsing dtype: unit, count 

6048 unit = cast("TimeUnit", unit) 

6049 # error: Argument "tz" to "tz_to_dtype" has incompatible type 

6050 # "str | tzinfo | None"; expected "tzinfo" 

6051 dtype = tz_to_dtype(tz=tz, unit=unit) # type: ignore[arg-type] 

6052 dta = DatetimeArray._from_sequence(values, dtype=dtype) 

6053 return dta 

6054 

6055 

6056def _read_index_nan_rep(attrs, name: str = "nan_rep") -> str | None: 

6057 """ 

6058 Read back the NaN sentinel persisted for a string Index column (GH#9604). 

6059 

6060 The writer records the attribute for every string Index, empty when the 

6061 index had no missing value, so its absence means the file predates the 

6062 sentinel. Such a file stored a NaN as the bare string "nan", which is how 

6063 pandas read it back before the sentinel existed. 

6064 """ 

6065 nan_rep = getattr(attrs, name, "nan") 

6066 return nan_rep or None 

6067 

6068 

6069def _make_nan_rep( 

6070 values: np.ndarray, mask: np.ndarray, existing: set | None = None 

6071) -> str: 

6072 """ 

6073 Choose a string NaN sentinel for a string column that does not collide with 

6074 any non-missing value, so genuine missing values round-trip without being 

6075 confused with a literal string such as ``"nan"`` (GH#9604). 

6076 

6077 ``existing`` holds values already stored for this column in a prior 

6078 ``table``-format append; the sentinel must avoid those too, since a single 

6079 sentinel is persisted for the whole column. 

6080 """ 

6081 non_missing = values[~mask] 

6082 if lib.infer_dtype(non_missing, skipna=True) != "string": 

6083 # Only a str can equal the sentinel, and an elementwise == against a 

6084 # list or ndarray element is not a bool. Dropping the rest also keeps an 

6085 # unhashable element from raising here, ahead of the "Cannot serialize 

6086 # the column" message it deserves. 

6087 non_missing = np.array( 

6088 [val for val in non_missing if isinstance(val, str)], dtype=object 

6089 ) 

6090 nan_rep = "nan" 

6091 while (non_missing == nan_rep).any() or ( 

6092 existing is not None and nan_rep in existing 

6093 ): 

6094 nan_rep = f"_{nan_rep}_" 

6095 return nan_rep 

6096 

6097 

6098def _read_stored_column_values(table, cname: str, encoding: str, errors: str) -> set: 

6099 """ 

6100 Read the values already stored for a string column (GH#9604), so a NaN 

6101 sentinel chosen for a later append cannot collide with them. 

6102 """ 

6103 raw = getattr(table.cols, cname)[:] 

6104 decoded = _unconvert_string_array( 

6105 raw, nan_rep=None, encoding=encoding, errors=errors 

6106 ) 

6107 # a multi-column data block stores a 2-D array; one sentinel covers it all 

6108 return set(decoded.ravel()) 

6109 

6110 

6111def _make_data_col_nan_rep( 

6112 blk_values, 

6113 columns: list, 

6114 existing_col, 

6115 table, 

6116 encoding: str, 

6117 errors: str, 

6118) -> str | None: 

6119 """ 

6120 Choose the NaN sentinel for a string data column, mirroring the string-Index 

6121 handling (GH#9604): reuse the sentinel already persisted for the column, 

6122 otherwise mint a collision-free one, and only when the column actually has a 

6123 missing value. Returns None when no sentinel is needed (a literal "nan" then 

6124 round-trips because the read path skips substitution when the sentinel is 

6125 absent). 

6126 """ 

6127 if isinstance(blk_values.dtype, StringDtype): 

6128 blk_values = blk_values.to_numpy() 

6129 if blk_values.dtype != object: 

6130 return None 

6131 

6132 values = np.asarray(blk_values) 

6133 flat = values.reshape(-1) 

6134 mask = isna(flat) 

6135 nan_rep = existing_col.nan_rep if existing_col is not None else None 

6136 if mask.any() and nan_rep is None: 

6137 existing_values = None 

6138 if existing_col is not None and existing_col.kind == "string": 

6139 existing_values = _read_stored_column_values( 

6140 table, existing_col.cname, encoding, errors 

6141 ) 

6142 nan_rep = _make_nan_rep(flat, mask, existing_values) 

6143 if ( 

6144 nan_rep is not None 

6145 # an elementwise == against a list/ndarray element is not a bool, and a 

6146 # non-string column has _maybe_convert_for_string_atom's much better 

6147 # "Cannot serialize the column" error waiting for it anyway 

6148 and lib.infer_dtype(flat, skipna=True) == "string" 

6149 and (flat[~mask] == nan_rep).any() 

6150 ): 

6151 # A real value equal to the sentinel is indistinguishable from a 

6152 # missing value on read; refuse rather than silently corrupt it. 

6153 collides = (values == nan_rep) & ~isna(values) 

6154 # a block is (n_columns, n_rows), so the first axis picks the label 

6155 pos = int(np.argmax(collides.any(axis=1))) if collides.ndim == 2 else 0 

6156 label = columns[pos] 

6157 raise ValueError( 

6158 f"Cannot store the string {str(nan_rep)!r} in column [{label}]: it " 

6159 "collides with the sentinel used to encode missing values on disk" 

6160 ) 

6161 return nan_rep 

6162 

6163 

6164def _convert_index( 

6165 name: str, 

6166 index: Index, 

6167 encoding: str, 

6168 errors: str, 

6169 existing_nan_rep: str | None = None, 

6170 existing_values: set | None = None, 

6171) -> IndexCol: 

6172 assert isinstance(name, str) 

6173 

6174 index_name = index.name 

6175 # GH#26144, GH#38305, GH#42070: most extension dtypes cannot be stored as 

6176 # an Index in HDF5. Allow the ones that have working code paths below 

6177 # (DatetimeTZ/Period via i8 conversion, BooleanDtype via is_bool_dtype, 

6178 # StringDtype, CategoricalDtype) and reject everything else early before 

6179 # _dtype_to_kind / _get_atom choke with a low-level error. 

6180 # BooleanDtype round-trips lossily but does not error on main, so it 

6181 # remains carved out here. 

6182 if ( 

6183 isinstance(index.dtype, ExtensionDtype) 

6184 and not needs_i8_conversion(index.dtype) 

6185 and not is_bool_dtype(index.dtype) 

6186 and not isinstance(index.dtype, (StringDtype, CategoricalDtype)) 

6187 ): 

6188 raise NotImplementedError( 

6189 f"Cannot store an Index with dtype {index.dtype} in an HDF5 file. " 

6190 "HDFStore supports NumPy dtypes, datetime64 with timezone, " 

6191 "categorical, and string extension types." 

6192 ) 

6193 

6194 if isinstance(index.dtype, CategoricalDtype): 

6195 # GH#33909, GH#16118: round-trip a CategoricalIndex by storing its 

6196 # integer codes as the column data and the categories as metadata, 

6197 # mirroring how a CategoricalDtype data column is persisted. 

6198 cat = cast("Categorical", index._values) 

6199 converted, dtype_name = _get_data_and_dtype_name(cat) 

6200 kind = _dtype_to_kind(dtype_name) 

6201 atom = DataIndexableCol._get_atom(converted) 

6202 return IndexCol( 

6203 name, 

6204 values=converted, 

6205 kind=kind, 

6206 typ=atom, 

6207 index_name=index_name, 

6208 meta="category", 

6209 metadata=np.asarray(cat.categories).ravel(), 

6210 ordered=cat.ordered, 

6211 ) 

6212 

6213 # error: Argument 1 to "_get_data_and_dtype_name" has incompatible type "Index"; 

6214 # expected "Union[ExtensionArray, ndarray]" 

6215 converted, dtype_name = _get_data_and_dtype_name(index) # type: ignore[arg-type] 

6216 kind = _dtype_to_kind(dtype_name) 

6217 atom = DataIndexableCol._get_atom(converted) 

6218 

6219 if ( 

6220 lib.is_np_dtype(index.dtype, "iu") 

6221 or needs_i8_conversion(index.dtype) 

6222 or is_bool_dtype(index.dtype) 

6223 ): 

6224 # Includes Index, RangeIndex, DatetimeIndex, TimedeltaIndex, PeriodIndex, 

6225 # in which case "kind" is "integer", "integer", "datetime64", 

6226 # "timedelta64", and "integer", respectively. 

6227 return IndexCol( 

6228 name, 

6229 values=converted, 

6230 kind=kind, 

6231 typ=atom, 

6232 freq=getattr(index, "freq", None), 

6233 tz=getattr(index, "tz", None), 

6234 index_name=index_name, 

6235 ) 

6236 

6237 if isinstance(index, MultiIndex): 

6238 raise TypeError("MultiIndex not supported here!") 

6239 

6240 inferred_type = lib.infer_dtype(index, skipna=False) 

6241 # we won't get inferred_type of "datetime64" or "timedelta64" as these 

6242 # would go through the DatetimeIndex/TimedeltaIndex paths above 

6243 

6244 values = np.asarray(index) 

6245 

6246 if inferred_type == "date": 

6247 converted = np.asarray([v.toordinal() for v in values], dtype=np.int32) 

6248 return IndexCol( 

6249 name, converted, "date", _tables().Time32Col(), index_name=index_name 

6250 ) 

6251 elif inferred_type == "string": 

6252 # GH#9604: a genuine missing value and the literal string "nan" both 

6253 # serialize to b"nan" via the fixed-width string cast, so they cannot 

6254 # be told apart on read. Substitute the missing values with a sentinel 

6255 # that does not collide with any real value and persist it (see 

6256 # write_index / IndexCol.set_attr) so the reader can restore the NAs 

6257 # while leaving a literal "nan" untouched. 

6258 mask = np.asarray(index.isna()) 

6259 # Reuse the sentinel already persisted for this column (a ``table`` 

6260 # append) so it stays stable across chunks; only mint a fresh one when 

6261 # none exists yet, avoiding every value already stored (existing_values) 

6262 # as well as the ones in this chunk. 

6263 nan_rep = existing_nan_rep 

6264 if mask.any() and nan_rep is None: 

6265 nan_rep = _make_nan_rep(values, mask, existing_values) 

6266 if nan_rep is not None: 

6267 if (values[~mask] == nan_rep).any(): 

6268 # A real value equal to the sentinel is indistinguishable from a 

6269 # missing value on read; refuse rather than silently corrupt it. 

6270 raise ValueError( 

6271 f"Cannot store the string {str(nan_rep)!r} in Index column " 

6272 f"[{name}]: it collides with the sentinel used to encode " 

6273 "missing values in this column" 

6274 ) 

6275 if mask.any(): 

6276 values = values.copy() 

6277 values[mask] = nan_rep 

6278 converted = _convert_string_array(values, encoding, errors) 

6279 itemsize = converted.dtype.itemsize 

6280 return IndexCol( 

6281 name, 

6282 converted, 

6283 "string", 

6284 _tables().StringCol(itemsize), 

6285 index_name=index_name, 

6286 nan_rep=nan_rep, 

6287 ) 

6288 

6289 elif inferred_type in ["integer", "floating"]: 

6290 return IndexCol( 

6291 name, values=converted, kind=kind, typ=atom, index_name=index_name 

6292 ) 

6293 else: 

6294 assert isinstance(converted, np.ndarray) and converted.dtype == object 

6295 assert kind == "object", kind 

6296 atom = _tables().ObjectAtom() 

6297 return IndexCol(name, converted, kind, atom, index_name=index_name) 

6298 

6299 

6300def _unconvert_index( 

6301 data, kind: str, encoding: str, errors: str, nan_rep=None 

6302) -> np.ndarray | Index: 

6303 index: Index | np.ndarray 

6304 

6305 if kind.startswith("datetime64"): 

6306 if kind == "datetime64": 

6307 # created before we stored resolution information 

6308 index = DatetimeIndex(data, copy=False) 

6309 else: 

6310 index = DatetimeIndex(data.view(kind), copy=False) 

6311 elif kind.startswith("timedelta64"): 

6312 if kind == "timedelta64": 

6313 # created before we stored resolution information 

6314 index = TimedeltaIndex(data, copy=False) 

6315 else: 

6316 index = TimedeltaIndex(data.view(kind), copy=False) 

6317 elif kind == "date": 

6318 try: 

6319 index = np.asarray([date.fromordinal(v) for v in data], dtype=object) 

6320 except ValueError: 

6321 index = np.asarray([date.fromtimestamp(v) for v in data], dtype=object) 

6322 elif kind in ("integer", "float", "bool"): 

6323 index = np.asarray(data) 

6324 elif kind in ("string"): 

6325 index = _unconvert_string_array( 

6326 data, nan_rep=nan_rep, encoding=encoding, errors=errors 

6327 ) 

6328 elif kind == "object": 

6329 index = np.asarray(data[0]) 

6330 else: # pragma: no cover 

6331 raise ValueError(f"unrecognized index type {kind}") 

6332 return index 

6333 

6334 

6335def _maybe_convert_for_string_atom( 

6336 name: str, 

6337 bvalues: ArrayLike, 

6338 existing_col, 

6339 min_itemsize, 

6340 nan_rep, 

6341 encoding, 

6342 errors, 

6343 columns: list[str], 

6344): 

6345 if isinstance(bvalues.dtype, StringDtype): 

6346 bvalues = bvalues.to_numpy() 

6347 if bvalues.dtype != object: 

6348 return bvalues 

6349 

6350 bvalues = cast("np.ndarray", bvalues) 

6351 

6352 dtype_name = bvalues.dtype.name 

6353 inferred_type = lib.infer_dtype(bvalues, skipna=False) 

6354 

6355 if inferred_type == "date": 

6356 raise TypeError("[date] is not implemented as a table column") 

6357 if inferred_type == "datetime": 

6358 # after GH#8260 

6359 # this only would be hit for a multi-timezone dtype which is an error 

6360 raise TypeError( 

6361 "too many timezones in this block, create separate data columns" 

6362 ) 

6363 

6364 if not (inferred_type == "string" or dtype_name == "object"): 

6365 return bvalues 

6366 

6367 mask = isna(bvalues) 

6368 data = bvalues.copy() 

6369 data[mask] = nan_rep 

6370 

6371 if existing_col and mask.any() and len(nan_rep) > existing_col.itemsize: 

6372 raise ValueError( 

6373 "NaN representation is too large for existing column size: " 

6374 f"{str(nan_rep)!r} has length {len(nan_rep)} but this column has a " 

6375 f"limit of {existing_col.itemsize}!\n" 

6376 "It can be longer than the values themselves. Recreate the table " 

6377 "passing min_itemsize to reserve room for it." 

6378 ) 

6379 

6380 # see if we have a valid string type 

6381 inferred_type = lib.infer_dtype(data, skipna=False) 

6382 if inferred_type != "string": 

6383 # we cannot serialize this data, so report an exception on a column 

6384 # by column basis 

6385 

6386 # expected behaviour: 

6387 # search block for a non-string object column by column 

6388 for i in range(data.shape[0]): 

6389 col = data[i] 

6390 inferred_type = lib.infer_dtype(col, skipna=False) 

6391 if inferred_type != "string": 

6392 error_column_label = columns[i] if len(columns) > i else f"No.{i}" 

6393 raise TypeError( 

6394 f"Cannot serialize the column [{error_column_label}]\n" 

6395 f"because its data contents are not [string] but " 

6396 f"[{inferred_type}] object dtype" 

6397 ) 

6398 

6399 # itemsize is the maximum length of a string (along any dimension) 

6400 

6401 data_converted = _convert_string_array(data, encoding, errors).reshape(data.shape) 

6402 itemsize = data_converted.itemsize 

6403 

6404 # specified min_itemsize? 

6405 if isinstance(min_itemsize, dict): 

6406 min_itemsize = int(min_itemsize.get(name) or min_itemsize.get("values") or 0) 

6407 itemsize = max(min_itemsize or 0, itemsize) 

6408 

6409 # check for column in the values conflicts 

6410 if existing_col is not None: 

6411 eci = existing_col.validate_col(itemsize) 

6412 if eci is not None and eci > itemsize: 

6413 itemsize = eci 

6414 

6415 data_converted = data_converted.astype(f"|S{itemsize}", copy=False) 

6416 return data_converted 

6417 

6418 

6419def _convert_string_array(data: np.ndarray, encoding: str, errors: str) -> np.ndarray: 

6420 """ 

6421 Take a string-like that is object dtype and coerce to a fixed size string type. 

6422 

6423 Parameters 

6424 ---------- 

6425 data : np.ndarray[object] 

6426 encoding : str 

6427 errors : str 

6428 Handler for encoding errors. 

6429 

6430 Returns 

6431 ------- 

6432 np.ndarray[fixed-length-string] 

6433 """ 

6434 # encode if needed 

6435 if len(data): 

6436 data = ( 

6437 Series(data.ravel(), copy=False, dtype="object") 

6438 .str.encode(encoding, errors) 

6439 ._values.reshape(data.shape) 

6440 ) 

6441 

6442 # create the sized dtype 

6443 ensured = ensure_object(data.ravel()) 

6444 itemsize = max(1, libwriters.max_len_string_array(ensured)) 

6445 

6446 data = np.asarray(data, dtype=f"S{itemsize}") 

6447 return data 

6448 

6449 

6450def _unconvert_string_array( 

6451 data: np.ndarray, nan_rep, encoding: str, errors: str 

6452) -> np.ndarray: 

6453 """ 

6454 Inverse of _convert_string_array. 

6455 

6456 Parameters 

6457 ---------- 

6458 data : np.ndarray[fixed-length-string] 

6459 nan_rep : the storage repr of NaN, or None to skip substitution. 

6460 Pass None when the writer did not encode NaN as a sentinel string; 

6461 otherwise legitimate occurrences of the sentinel value would be 

6462 incorrectly replaced with NaN on read. 

6463 encoding : str 

6464 errors : str 

6465 Handler for encoding errors. 

6466 

6467 Returns 

6468 ------- 

6469 np.ndarray[object] 

6470 Decoded data. 

6471 """ 

6472 shape = data.shape 

6473 data = np.asarray(data.ravel(), dtype=object) 

6474 

6475 if len(data): 

6476 itemsize = libwriters.max_len_string_array(ensure_object(data)) 

6477 dtype = f"U{itemsize}" 

6478 

6479 if isinstance(data[0], bytes): 

6480 ser = Series(data, copy=False).str.decode( 

6481 encoding, errors=errors, dtype="object" 

6482 ) 

6483 data = ser.to_numpy() 

6484 data.flags.writeable = True 

6485 else: 

6486 data = data.astype(dtype, copy=False).astype(object, copy=False) 

6487 

6488 if nan_rep is not None: 

6489 libwriters.string_array_replace_from_nan_rep(data, nan_rep) 

6490 return data.reshape(shape) 

6491 

6492 

6493def _maybe_convert( 

6494 values: np.ndarray, val_kind: str, encoding: str, errors: str, nan_rep=None 

6495): 

6496 assert isinstance(val_kind, str), type(val_kind) 

6497 if _need_convert(val_kind): 

6498 conv = _get_converter(val_kind, encoding, errors, nan_rep) 

6499 values = conv(values) 

6500 return values 

6501 

6502 

6503def _get_converter(kind: str, encoding: str, errors: str, nan_rep=None): 

6504 if kind == "datetime64": 

6505 return lambda x: np.asarray(x, dtype="M8[ns]") 

6506 elif "datetime64" in kind: 

6507 return lambda x: np.asarray(x, dtype=kind) 

6508 elif kind == "string": 

6509 return lambda x: _unconvert_string_array( 

6510 x, nan_rep=nan_rep, encoding=encoding, errors=errors 

6511 ) 

6512 else: # pragma: no cover 

6513 raise ValueError(f"invalid kind {kind}") 

6514 

6515 

6516def _need_convert(kind: str) -> bool: 

6517 if kind in ("datetime64", "string") or "datetime64" in kind: 

6518 return True 

6519 return False 

6520 

6521 

6522def _dtype_to_kind(dtype_str: str) -> str: 

6523 """ 

6524 Find the "kind" string describing the given dtype name. 

6525 """ 

6526 if dtype_str.startswith(("string", "bytes")): 

6527 kind = "string" 

6528 elif dtype_str.startswith("float"): 

6529 kind = "float" 

6530 elif dtype_str.startswith("complex"): 

6531 kind = "complex" 

6532 elif dtype_str.startswith(("int", "uint")): 

6533 kind = "integer" 

6534 elif dtype_str.startswith("datetime64"): 

6535 kind = dtype_str 

6536 elif dtype_str.startswith("timedelta"): 

6537 kind = dtype_str 

6538 elif dtype_str.startswith("bool"): 

6539 kind = "bool" 

6540 elif dtype_str.startswith("category"): 

6541 kind = "category" 

6542 elif dtype_str.startswith("period"): 

6543 # We store the `freq` attr so we can restore from integers 

6544 kind = "integer" 

6545 elif dtype_str == "object": 

6546 kind = "object" 

6547 elif dtype_str == "str": 

6548 kind = "str" 

6549 else: 

6550 raise ValueError(f"cannot interpret dtype of [{dtype_str}]") 

6551 

6552 return kind 

6553 

6554 

6555def _get_data_and_dtype_name(data: ArrayLike): 

6556 """ 

6557 Convert the passed data into a storable form and a dtype string. 

6558 """ 

6559 if isinstance(data, Categorical): 

6560 data = data.codes 

6561 

6562 if isinstance(data.dtype, DatetimeTZDtype): 

6563 # For datetime64tz we need to drop the TZ in tests TODO: why? 

6564 dtype_name = f"datetime64[{data.dtype.unit}]" 

6565 else: 

6566 dtype_name = data.dtype.name 

6567 

6568 if data.dtype.kind in "mM": 

6569 data = np.asarray(data.view("i8")) 

6570 # TODO: we used to reshape for the dt64tz case, but no longer 

6571 # doing that doesn't seem to break anything. why? 

6572 

6573 elif isinstance(data, (PeriodIndex, PeriodArray)): 

6574 data = data.asi8 

6575 

6576 data = np.asarray(data) 

6577 return data, dtype_name 

6578 

6579 

6580def _or_of_ands_columns(condition) -> set[str]: 

6581 """ 

6582 Look for an OR with at least one AND operand, e.g. ``(A & B) | (C & D)`` 

6583 or ``(A & B) | C``, in a pruned PyTables condition tree. The AND operand 

6584 need not span two columns -- ``(A > 1 & A < 5) | B`` counts too. 

6585 

6586 This is the query shape that can trigger an upstream PyTables bug where 

6587 index-accelerated reads silently drop matching rows (GH#50598). Return the 

6588 set of column names referenced by the AND-ed operands of such an OR, or an 

6589 empty set if the pattern is not present. 

6590 

6591 Only the AND-ed operands matter: the bug needs one of *their* columns to be 

6592 indexed. Indexing a column that appears solely as a bare OR operand (the 

6593 ``C`` of ``(A & B) | C``) does not trigger it. 

6594 """ 

6595 cols: set[str] = set() 

6596 

6597 def collect(node) -> None: 

6598 """Add every column name referenced under ``node``.""" 

6599 if not isinstance(node, JointConditionBinOp): 

6600 # leaf comparison: lhs.value is the column name 

6601 cols.add(node.lhs.value) 

6602 return 

6603 collect(node.lhs) 

6604 collect(node.rhs) 

6605 

6606 def visit(node) -> None: 

6607 if not isinstance(node, JointConditionBinOp): 

6608 return 

6609 if node.op == "|": 

6610 for operand in (node.lhs, node.rhs): 

6611 if isinstance(operand, JointConditionBinOp) and operand.op == "&": 

6612 collect(operand) 

6613 visit(node.lhs) 

6614 visit(node.rhs) 

6615 

6616 visit(condition) 

6617 return cols 

6618 

6619 

6620class Selection: 

6621 """ 

6622 Carries out a selection operation on a tables.Table object. 

6623 

6624 Parameters 

6625 ---------- 

6626 table : a Table object 

6627 where : list of Terms (or convertible to) 

6628 start, stop: indices to start and/or stop selection 

6629 

6630 """ 

6631 

6632 def __init__( 

6633 self, 

6634 table: Table, 

6635 where=None, 

6636 start: int | None = None, 

6637 stop: int | None = None, 

6638 ) -> None: 

6639 self.table = table 

6640 self.where = where 

6641 self.start = start 

6642 self.stop = stop 

6643 self.condition = None 

6644 self.filter = None 

6645 self.terms = None 

6646 self.coordinates = None 

6647 

6648 if is_list_like(where): 

6649 # see if we have a passed coordinate like 

6650 inferred = lib.infer_dtype(where, skipna=False) 

6651 if inferred in ("integer", "boolean"): 

6652 where = np.asarray(where) 

6653 # start/stop are None on the read_coordinates paths, which is how 

6654 # an out-of-range coordinate used to reach PyTables unchecked 

6655 start, stop = _resolve_row_window( 

6656 self.start, self.stop, self.table.nrows 

6657 ) 

6658 if where.dtype == np.bool_: 

6659 self.coordinates = np.arange(start, stop)[where] 

6660 elif issubclass(where.dtype.type, np.integer): 

6661 invalid = where[(where < start) | (where >= stop)] 

6662 if len(invalid): 

6663 raise ValueError( 

6664 "where must have index locations >= start and < stop, " 

6665 f"but got {invalid[:5].tolist()}" 

6666 f"{' ...' if len(invalid) > 5 else ''} " 

6667 f"with start={start} and stop={stop}" 

6668 ) 

6669 self.coordinates = where 

6670 

6671 if self.coordinates is None: 

6672 self.terms = self.generate(where) 

6673 

6674 # create the numexpr & the filter 

6675 if self.terms is not None: 

6676 self.condition, self.filter = self.terms.evaluate() 

6677 

6678 def _warn_if_unreliable_or(self) -> None: 

6679 """ 

6680 Warn for nested-OR queries with AND-ed operands over indexed columns 

6681 (e.g. ``(A & B) | (C & D)``) that can return incorrect results due to 

6682 an upstream PyTables bug (GH#50598). 

6683 

6684 Called from the query methods rather than ``__init__``: one read builds 

6685 several ``Selection`` objects for the same ``where``, and only the one 

6686 that runs the condition should warn. 

6687 """ 

6688 table = getattr(self.table, "table", None) 

6689 if table is None: 

6690 return 

6691 cols = _or_of_ands_columns(self.condition) 

6692 if not any( 

6693 table.colinstances[name].is_indexed 

6694 for name in cols 

6695 if name in table.colnames 

6696 ): 

6697 return 

6698 warnings.warn( 

6699 "Querying with a nested 'where' that ORs AND-ed conditions over " 

6700 "indexed columns (e.g. '(A & B) | (C & D)') can silently match the " 

6701 "wrong rows due to an upstream PyTables bug (GH#50598). To get " 

6702 "correct results, write the table with 'index=False', or split the " 

6703 "'where' into one operation per OR branch.", 

6704 UserWarning, 

6705 stacklevel=find_stack_level(), 

6706 ) 

6707 

6708 @overload 

6709 def generate(self, where: dict | list | tuple | str) -> PyTablesExpr: ... 

6710 

6711 @overload 

6712 def generate(self, where: None) -> None: ... 

6713 

6714 def generate(self, where: dict | list | tuple | str | None) -> PyTablesExpr | None: 

6715 """where can be a : dict,list,tuple,string""" 

6716 if where is None: 

6717 return None 

6718 

6719 q = self.table.queryables() 

6720 try: 

6721 return PyTablesExpr(where, queryables=q, encoding=self.table.encoding) 

6722 except NameError as err: 

6723 # raise a nice message, suggesting that the user should use 

6724 # data_columns 

6725 qkeys = ",".join(q.keys()) 

6726 msg = dedent( 

6727 f"""\ 

6728 The passed where expression: {where} 

6729 contains an invalid variable reference 

6730 all of the variable references must be a reference to 

6731 an axis (e.g. 'index' or 'columns'), or a data_column 

6732 The currently defined references are: {qkeys} 

6733 """ 

6734 ) 

6735 raise ValueError(msg) from err 

6736 

6737 def select(self): 

6738 """ 

6739 generate the selection 

6740 """ 

6741 if self.condition is not None: 

6742 self._warn_if_unreliable_or() 

6743 try: 

6744 return self.table.table.read_where( 

6745 self.condition.format(), start=self.start, stop=self.stop 

6746 ) 

6747 except ValueError as err: 

6748 # GH#39752 PyTables queries indexed columns by combining one 

6749 # boolean array per comparison term in a single numexpr call, 

6750 # capped at NPY_MAXARGS-1 inputs (a numexpr build-time 

6751 # constant, 32 or 64 depending on the targeted numpy). 

6752 # Translate the opaque numexpr error into actionable guidance. 

6753 if "too many inputs" not in str(err): 

6754 raise 

6755 raise ValueError( 

6756 "The passed where expression has too many comparisons " 

6757 "for a query against indexed columns (a numexpr " 

6758 "limitation). Reduce the number of comparisons, or store " 

6759 "the table with 'index=False' (e.g. " 

6760 "DataFrame.to_hdf(..., index=False)) so the query does " 

6761 "not use the column index." 

6762 ) from err 

6763 elif self.coordinates is not None: 

6764 return self.table.table.read_coordinates(self.coordinates) 

6765 return self.table.table.read(start=self.start, stop=self.stop) 

6766 

6767 def select_coords(self): 

6768 """ 

6769 generate the selection 

6770 """ 

6771 start, stop = _resolve_row_window(self.start, self.stop, self.table.nrows) 

6772 

6773 if self.condition is not None: 

6774 self._warn_if_unreliable_or() 

6775 coords = self.table.table.get_where_list( 

6776 self.condition.format(), start=start, stop=stop, sort=True 

6777 ) 

6778 elif self.coordinates is not None: 

6779 return self.coordinates 

6780 else: 

6781 coords = np.arange(start, stop) 

6782 

6783 if self.filter is not None and len(coords): 

6784 # a term the numexpr condition can't carry -- e.g. an "index in 

6785 # [...]" clause with more selectors than numexpr can handle -- is 

6786 # realized as a post-read filter (GH#17567). A where clause may 

6787 # combine such a filter with a numexpr condition (e.g. "index in 

6788 # [...] and A>=1"), so it must be applied on top of get_where_list's 

6789 # coordinates here -- this also keeps the row-coordinate path 

6790 # (iterator / chunksize) dropping the same rows as a plain read 

6791 # (GH#12953). 

6792 for field, op, filt in self.filter.format(): 

6793 data = self.table.read_column( 

6794 field, start=coords.min(), stop=coords.max() + 1 

6795 ) 

6796 coords = coords[op(data.iloc[coords - coords.min()], filt).values] 

6797 return coords