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

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

486 statements  

1"""Common I/O API utilities""" 

2 

3from __future__ import annotations 

4 

5from abc import ( 

6 ABC, 

7 abstractmethod, 

8) 

9import codecs 

10from collections import defaultdict 

11from collections.abc import ( 

12 Hashable, 

13 Mapping, 

14 Sequence, 

15) 

16import dataclasses 

17import functools 

18import gzip 

19from io import ( 

20 BufferedIOBase, 

21 BytesIO, 

22 IOBase, 

23 RawIOBase, 

24 StringIO, 

25 TextIOBase, 

26 TextIOWrapper, 

27 UnsupportedOperation, 

28) 

29import mmap 

30import os 

31from pathlib import Path 

32import re 

33import tarfile 

34from typing import ( 

35 IO, 

36 TYPE_CHECKING, 

37 Any, 

38 AnyStr, 

39 Generic, 

40 Literal, 

41 TypeVar, 

42 cast, 

43 overload, 

44) 

45from urllib.parse import ( 

46 urljoin, 

47 urlparse as parse_url, 

48 uses_netloc, 

49 uses_params, 

50 uses_relative, 

51) 

52import warnings 

53import zipfile 

54 

55from pandas._typing import ( 

56 BaseBuffer, 

57 ReadCsvBuffer, 

58) 

59from pandas.compat._optional import import_optional_dependency 

60from pandas.util._exceptions import find_stack_level 

61 

62from pandas.core.dtypes.common import ( 

63 is_bool, 

64 is_file_like, 

65 is_integer, 

66 is_list_like, 

67) 

68from pandas.core.dtypes.generic import ABCMultiIndex 

69 

70_VALID_URLS = set(uses_relative + uses_netloc + uses_params) 

71_VALID_URLS.discard("") 

72_FSSPEC_URL_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9+\-+.]*(::[A-Za-z0-9+\-+.]+)*://") 

73 

74BaseBufferT = TypeVar("BaseBufferT", bound=BaseBuffer) 

75 

76 

77if TYPE_CHECKING: 

78 from types import TracebackType 

79 

80 from pandas._typing import ( 

81 CompressionDict, 

82 CompressionOptions, 

83 DtypeArg, 

84 FilePath, 

85 ReadBuffer, 

86 StorageOptions, 

87 WriteBuffer, 

88 ) 

89 

90 from pandas import MultiIndex 

91 

92 

93@dataclasses.dataclass 

94class IOArgs: 

95 """ 

96 Return value of io/common.py:_get_filepath_or_buffer. 

97 """ 

98 

99 filepath_or_buffer: str | BaseBuffer 

100 encoding: str 

101 mode: str 

102 compression: CompressionDict 

103 should_close: bool = False 

104 close_handles: list[Any] = dataclasses.field(default_factory=list) 

105 

106 

107@dataclasses.dataclass 

108class IOHandles(Generic[AnyStr]): 

109 """ 

110 Return value of io/common.py:get_handle 

111 

112 Can be used as a context manager. 

113 

114 This is used to easily close created buffers and to handle corner cases when 

115 TextIOWrapper is inserted. 

116 

117 handle: The file handle to be used. 

118 created_handles: All file handles that are created by get_handle 

119 is_wrapped: Whether a TextIOWrapper needs to be detached. 

120 """ 

121 

122 # handle might not implement the IO-interface 

123 handle: IO[AnyStr] 

124 compression: CompressionDict 

125 created_handles: list[IO[bytes] | IO[str]] = dataclasses.field(default_factory=list) 

126 is_wrapped: bool = False 

127 

128 def close(self) -> None: 

129 """ 

130 Close all created buffers. 

131 

132 Note: If a TextIOWrapper was inserted, it is flushed and detached to 

133 avoid closing the potentially user-created buffer. 

134 """ 

135 if self.is_wrapped: 

136 assert isinstance(self.handle, TextIOWrapper) 

137 self.handle.flush() 

138 self.handle.detach() 

139 self.created_handles.remove(self.handle) 

140 for handle in self.created_handles: 

141 handle.close() 

142 self.created_handles = [] 

143 self.is_wrapped = False 

144 

145 def __enter__(self) -> IOHandles[AnyStr]: 

146 return self 

147 

148 def __exit__( 

149 self, 

150 exc_type: type[BaseException] | None, 

151 exc_value: BaseException | None, 

152 traceback: TracebackType | None, 

153 ) -> None: 

154 self.close() 

155 

156 

157def is_url(url: object) -> bool: 

158 """ 

159 Check to see if a URL has a valid protocol. 

160 

161 Parameters 

162 ---------- 

163 url : str or unicode 

164 

165 Returns 

166 ------- 

167 isurl : bool 

168 If `url` has a valid protocol return True otherwise False. 

169 """ 

170 if not isinstance(url, str): 

171 return False 

172 return parse_url(url).scheme in _VALID_URLS 

173 

174 

175@overload 

176def _expand_user(filepath_or_buffer: str) -> str: ... 

177 

178 

179@overload 

180def _expand_user(filepath_or_buffer: BaseBufferT) -> BaseBufferT: ... 

181 

182 

183def _expand_user(filepath_or_buffer: str | BaseBufferT) -> str | BaseBufferT: 

184 """ 

185 Return the argument with an initial component of ~ or ~user 

186 replaced by that user's home directory. 

187 

188 Parameters 

189 ---------- 

190 filepath_or_buffer : object to be converted if possible 

191 

192 Returns 

193 ------- 

194 expanded_filepath_or_buffer : an expanded filepath or the 

195 input if not expandable 

196 """ 

197 if isinstance(filepath_or_buffer, str): 

198 return os.path.expanduser(filepath_or_buffer) 

199 return filepath_or_buffer 

200 

201 

202def validate_header_arg(header: object) -> None: 

203 if header is None: 

204 return 

205 if is_integer(header): 

206 header = cast("int", header) 

207 if header < 0: 

208 # GH 27779 

209 raise ValueError( 

210 "Passing negative integer to header is invalid. " 

211 "For no header, use header=None instead" 

212 ) 

213 return 

214 if is_list_like(header, allow_sets=False): 

215 header = cast("Sequence[int]", header) 

216 if not all(map(is_integer, header)): 

217 raise ValueError("header must be integer or list of integers") 

218 if any(i < 0 for i in header): 

219 raise ValueError("cannot specify multi-index header with negative integers") 

220 return 

221 if is_bool(header): 

222 raise TypeError( 

223 "Passing a bool to header is invalid. Use header=None for no header or " 

224 "header=int or list-like of ints to specify " 

225 "the row(s) making up the column names" 

226 ) 

227 # GH 16338 

228 raise ValueError("header must be integer or list of integers") 

229 

230 

231@overload 

232def stringify_path( 

233 filepath_or_buffer: FilePath, convert_file_like: bool = ... 

234) -> str: ... 

235 

236 

237@overload 

238def stringify_path( 

239 filepath_or_buffer: BaseBufferT, convert_file_like: bool = ... 

240) -> BaseBufferT: ... 

241 

242 

243def stringify_path( 

244 filepath_or_buffer: FilePath | BaseBufferT, 

245 convert_file_like: bool = False, 

246) -> str | BaseBufferT: 

247 """ 

248 Attempt to convert a path-like object to a string. 

249 

250 Parameters 

251 ---------- 

252 filepath_or_buffer : object to be converted 

253 

254 Returns 

255 ------- 

256 str_filepath_or_buffer : maybe a string version of the object 

257 

258 Notes 

259 ----- 

260 Objects supporting the fspath protocol are coerced 

261 according to its __fspath__ method. 

262 

263 Any other object is passed through unchanged, which includes bytes, 

264 strings, buffers, or anything else that's not even path-like. 

265 """ 

266 if ( 

267 not convert_file_like 

268 and is_file_like(filepath_or_buffer) 

269 and hasattr(filepath_or_buffer, "__iter__") 

270 ): 

271 # GH 38125: some fsspec objects implement os.PathLike but have already opened a 

272 # file. This prevents opening the file a second time. infer_compression calls 

273 # this function with convert_file_like=True to infer the compression. 

274 # Requiring __iter__ keeps py.path.local, whose read/write are convenience 

275 # methods, on the path branch (GH#71769). 

276 return cast("BaseBufferT", filepath_or_buffer) 

277 

278 if isinstance(filepath_or_buffer, os.PathLike): 

279 filepath_or_buffer = filepath_or_buffer.__fspath__() 

280 return _expand_user(filepath_or_buffer) 

281 

282 

283def urlopen(*args: Any, **kwargs: Any) -> Any: 

284 """ 

285 Lazy-import wrapper for stdlib urlopen, as that imports a big chunk of 

286 the stdlib. 

287 """ 

288 import urllib.request 

289 

290 return urllib.request.urlopen(*args, **kwargs) # noqa: TID251 

291 

292 

293def is_fsspec_url(url: FilePath | BaseBuffer) -> bool: 

294 """ 

295 Returns true if the given URL looks like 

296 something fsspec can handle 

297 """ 

298 return ( 

299 isinstance(url, str) 

300 and bool(_FSSPEC_URL_PATTERN.match(url)) 

301 and not url.startswith(("http://", "https://")) 

302 ) 

303 

304 

305def _get_filepath_or_buffer( 

306 filepath_or_buffer: FilePath | BaseBuffer, 

307 encoding: str = "utf-8", 

308 compression: CompressionOptions | None = None, 

309 mode: str = "r", 

310 storage_options: StorageOptions | None = None, 

311) -> IOArgs: 

312 """ 

313 If the filepath_or_buffer is a url, translate and return the buffer. 

314 Otherwise passthrough. 

315 

316 Parameters 

317 ---------- 

318 filepath_or_buffer : a url, filepath (str or pathlib.Path), 

319 or buffer 

320 

321 compression : str or dict, default 'infer' 

322 For on-the-fly compression of the output data. If 'infer' and 

323 'filepath_or_buffer' is path-like, then detect compression from the 

324 following extensions: '.gz', 

325 '.bz2', '.zip', '.xz', '.zst', '.tar', '.tar.gz', '.tar.xz' or '.tar.bz2' 

326 (otherwise no compression). 

327 Set to ``None`` for no compression. 

328 Can also be a dict with key ``'method'`` set 

329 to one of {``'zip'``, ``'gzip'``, ``'bz2'``, ``'zstd'``, ``'xz'``, ``'tar'``} 

330 and other key-value pairs are forwarded to 

331 ``zipfile.ZipFile``, ``gzip.GzipFile``, 

332 ``bz2.BZ2File``, ``zstandard.ZstdCompressor``, ``lzma.LZMAFile`` or 

333 ``tarfile.TarFile``, respectively. 

334 As an example, the following could be passed for faster compression and to 

335 create a reproducible gzip archive: 

336 ``compression={'method': 'gzip', 'compresslevel': 1, 'mtime': 1}``. 

337 

338 encoding : the encoding to use to decode bytes, default is 'utf-8' 

339 mode : str, optional 

340 

341 storage_options : dict, optional 

342 Extra options that make sense for a particular storage connection, e.g. 

343 host, port, username, password, etc. For HTTP(S) URLs the key-value pairs 

344 are forwarded to ``urllib.request.Request`` as header options. For other 

345 URLs (e.g. starting with "s3://", and "gcs://") the key-value pairs are 

346 forwarded to ``fsspec.open``. Please see ``fsspec`` and ``urllib`` for more 

347 details, and for more examples on storage options refer `here 

348 <https://pandas.pydata.org/docs/user_guide/io.html? 

349 highlight=storage_options#reading-writing-remote-files>`_. 

350 

351 

352 Returns the dataclass IOArgs. 

353 """ 

354 filepath_or_buffer = stringify_path(filepath_or_buffer) 

355 

356 # handle compression dict 

357 compression_method, compression = get_compression_method(compression) 

358 compression_method = infer_compression(filepath_or_buffer, compression_method) 

359 

360 # GH21227 internal compression is not used for non-binary handles. 

361 if compression_method and hasattr(filepath_or_buffer, "write") and "b" not in mode: 

362 warnings.warn( 

363 "compression has no effect when passing a non-binary object as input.", 

364 RuntimeWarning, 

365 stacklevel=find_stack_level(), 

366 ) 

367 compression_method = None 

368 

369 compression = dict(compression, method=compression_method) 

370 

371 # bz2 and xz do not write the byte order mark for utf-16 and utf-32 

372 # print a warning when writing such files 

373 if ( 

374 "w" in mode 

375 and compression_method in ["bz2", "xz"] 

376 and encoding in ["utf-16", "utf-32"] 

377 ): 

378 warnings.warn( 

379 f"{compression_method} will not write the byte order mark for {encoding}", 

380 UnicodeWarning, 

381 stacklevel=find_stack_level(), 

382 ) 

383 

384 if "a" in mode and compression_method in ["zip", "tar"]: 

385 # GH56778 

386 warnings.warn( 

387 "zip and tar do not support mode 'a' properly. " 

388 "This combination will result in multiple files with same name " 

389 "being added to the archive.", 

390 RuntimeWarning, 

391 stacklevel=find_stack_level(), 

392 ) 

393 

394 # Use binary mode when converting path-like objects to file-like objects (fsspec) 

395 # except when text mode is explicitly requested. The original mode is returned if 

396 # fsspec is not used. 

397 fsspec_mode = mode 

398 if "t" not in fsspec_mode and "b" not in fsspec_mode: 

399 fsspec_mode += "b" 

400 

401 if isinstance(filepath_or_buffer, str) and is_url(filepath_or_buffer): 

402 # TODO: fsspec can also handle HTTP via requests, but leaving this 

403 # unchanged. using fsspec appears to break the ability to infer if the 

404 # server responded with gzipped data 

405 storage_options = storage_options or {} 

406 

407 # waiting until now for importing to match intended lazy logic of 

408 # urlopen function defined elsewhere in this module 

409 import urllib.request 

410 

411 # assuming storage_options is to be interpreted as headers 

412 req_info = urllib.request.Request(filepath_or_buffer, headers=storage_options) 

413 with urlopen(req_info) as req: 

414 content_encoding = req.headers.get("Content-Encoding", None) 

415 if content_encoding == "gzip": 

416 # Override compression based on Content-Encoding header 

417 compression = {"method": "gzip"} 

418 reader = BytesIO(req.read()) 

419 return IOArgs( 

420 filepath_or_buffer=reader, 

421 encoding=encoding, 

422 compression=compression, 

423 should_close=True, 

424 mode=fsspec_mode, 

425 ) 

426 

427 if is_fsspec_url(filepath_or_buffer): 

428 assert isinstance( 

429 filepath_or_buffer, str 

430 ) # just to appease mypy for this branch 

431 # two special-case s3-like protocols; these have special meaning in Hadoop, 

432 # but are equivalent to just "s3" from fsspec's point of view 

433 # cc #11071 

434 if filepath_or_buffer.startswith("s3a://"): 

435 filepath_or_buffer = filepath_or_buffer.replace("s3a://", "s3://") 

436 if filepath_or_buffer.startswith("s3n://"): 

437 filepath_or_buffer = filepath_or_buffer.replace("s3n://", "s3://") 

438 fsspec = import_optional_dependency("fsspec") 

439 

440 # If botocore is installed we fallback to reading with anon=True 

441 # to allow reads from public buckets 

442 err_types_to_retry_with_anon: list[Any] = [] 

443 try: 

444 import_optional_dependency("botocore") 

445 from botocore.exceptions import ( 

446 ClientError, 

447 NoCredentialsError, 

448 ) 

449 

450 err_types_to_retry_with_anon = [ 

451 ClientError, 

452 NoCredentialsError, 

453 PermissionError, 

454 ] 

455 except ImportError: 

456 pass 

457 

458 try: 

459 open_file = fsspec.open( 

460 filepath_or_buffer, mode=fsspec_mode, **(storage_options or {}) 

461 ) 

462 file_obj = open_file.open() 

463 # GH 34626 Reads from Public Buckets without Credentials needs anon=True 

464 except tuple(err_types_to_retry_with_anon): 

465 if storage_options is None: 

466 storage_options = {"anon": True} 

467 else: 

468 # don't mutate user input. 

469 storage_options = dict(storage_options) 

470 storage_options["anon"] = True 

471 open_file = fsspec.open( 

472 filepath_or_buffer, mode=fsspec_mode, **(storage_options or {}) 

473 ) 

474 file_obj = open_file.open() 

475 

476 return IOArgs( 

477 filepath_or_buffer=file_obj, 

478 encoding=encoding, 

479 compression=compression, 

480 close_handles=[open_file], 

481 should_close=True, 

482 mode=fsspec_mode, 

483 ) 

484 elif storage_options: 

485 raise ValueError( 

486 "storage_options passed with file object or non-fsspec file path" 

487 ) 

488 

489 if isinstance(filepath_or_buffer, (str, bytes, mmap.mmap)): 

490 return IOArgs( 

491 filepath_or_buffer=_expand_user(filepath_or_buffer), 

492 encoding=encoding, 

493 compression=compression, 

494 should_close=False, 

495 mode=mode, 

496 ) 

497 

498 if not ( 

499 hasattr(filepath_or_buffer, "read") or hasattr(filepath_or_buffer, "write") 

500 ): 

501 msg = f"Invalid file path or buffer object type: {type(filepath_or_buffer)}" 

502 raise ValueError(msg) 

503 

504 return IOArgs( 

505 filepath_or_buffer=filepath_or_buffer, 

506 encoding=encoding, 

507 compression=compression, 

508 should_close=False, 

509 mode=mode, 

510 ) 

511 

512 

513def file_path_to_url(path: str) -> str: 

514 """ 

515 converts an absolute native path to a FILE URL. 

516 

517 Parameters 

518 ---------- 

519 path : a path in native format 

520 

521 Returns 

522 ------- 

523 a valid FILE URL 

524 """ 

525 # lazify expensive import (~30ms) 

526 from urllib.request import pathname2url 

527 

528 return urljoin("file:", pathname2url(path)) 

529 

530 

531extension_to_compression = { 

532 ".tar": "tar", 

533 ".tar.gz": "tar", 

534 ".tar.bz2": "tar", 

535 ".tar.xz": "tar", 

536 ".gz": "gzip", 

537 ".bz2": "bz2", 

538 ".zip": "zip", 

539 ".xz": "xz", 

540 ".zst": "zstd", 

541} 

542_supported_compressions = set(extension_to_compression.values()) 

543 

544 

545def get_compression_method( 

546 compression: CompressionOptions, 

547) -> tuple[str | None, CompressionDict]: 

548 """ 

549 Simplifies a compression argument to a compression method string and 

550 a mapping containing additional arguments. 

551 

552 Parameters 

553 ---------- 

554 compression : str or mapping 

555 If string, specifies the compression method. If mapping, value at key 

556 'method' specifies compression method. 

557 

558 Returns 

559 ------- 

560 tuple of ({compression method}, Optional[str] 

561 {compression arguments}, Dict[str, Any]) 

562 

563 Raises 

564 ------ 

565 ValueError on mapping missing 'method' key 

566 """ 

567 compression_method: str | None 

568 if isinstance(compression, Mapping): 

569 compression_args = dict(compression) 

570 try: 

571 compression_method = compression_args.pop("method") 

572 except KeyError as err: 

573 raise ValueError("If mapping, compression must have key 'method'") from err 

574 else: 

575 compression_args = {} 

576 compression_method = compression 

577 return compression_method, compression_args 

578 

579 

580def infer_compression( 

581 filepath_or_buffer: FilePath | BaseBuffer, compression: str | None 

582) -> str | None: 

583 """ 

584 Get the compression method for filepath_or_buffer. If compression='infer', 

585 the inferred compression method is returned. Otherwise, the input 

586 compression method is returned unchanged, unless it's invalid, in which 

587 case an error is raised. 

588 

589 Parameters 

590 ---------- 

591 filepath_or_buffer : str or file handle 

592 File path or object. 

593 

594 compression : str or dict, default 'infer' 

595 For on-the-fly compression of the output data. If 'infer' and 

596 'filepath_or_buffer' is path-like, then detect compression from the 

597 following extensions: '.gz', 

598 '.bz2', '.zip', '.xz', '.zst', '.tar', '.tar.gz', '.tar.xz' or '.tar.bz2' 

599 (otherwise no compression). 

600 Set to ``None`` for no compression. 

601 Can also be a dict with key ``'method'`` set 

602 to one of {``'zip'``, ``'gzip'``, ``'bz2'``, ``'zstd'``, ``'xz'``, ``'tar'``} 

603 and other key-value pairs are forwarded to 

604 ``zipfile.ZipFile``, ``gzip.GzipFile``, 

605 ``bz2.BZ2File``, ``zstandard.ZstdCompressor``, ``lzma.LZMAFile`` or 

606 ``tarfile.TarFile``, respectively. 

607 As an example, the following could be passed for faster compression and to 

608 create a reproducible gzip archive: 

609 ``compression={'method': 'gzip', 'compresslevel': 1, 'mtime': 1}``. 

610 

611 Returns 

612 ------- 

613 string or None 

614 

615 Raises 

616 ------ 

617 ValueError on invalid compression specified. 

618 """ 

619 if compression is None: 

620 return None 

621 

622 # Infer compression 

623 if compression == "infer": 

624 # Convert all path types (e.g. pathlib.Path) to strings 

625 if isinstance(filepath_or_buffer, str) and "::" in filepath_or_buffer: 

626 # chained URLs contain :: 

627 filepath_or_buffer = filepath_or_buffer.split("::")[0] 

628 filepath_or_buffer = stringify_path(filepath_or_buffer, convert_file_like=True) 

629 if not isinstance(filepath_or_buffer, str): 

630 # Cannot infer compression of a buffer, assume no compression 

631 return None 

632 

633 # Infer compression from the filename/URL extension 

634 for extension, compression in extension_to_compression.items(): 

635 if filepath_or_buffer.lower().endswith(extension): 

636 return compression 

637 return None 

638 

639 # Compression has been specified. Check that it's valid 

640 if compression in _supported_compressions: 

641 return compression 

642 

643 valid = ["infer", None, *sorted(_supported_compressions)] 

644 msg = ( 

645 f"Unrecognized compression type: {compression}\n" 

646 f"Valid compression types are {valid}" 

647 ) 

648 raise ValueError(msg) 

649 

650 

651def check_parent_directory(path: Path | str) -> None: 

652 """ 

653 Check if parent directory of a file exists, raise OSError if it does not 

654 

655 Parameters 

656 ---------- 

657 path: Path or str 

658 Path to check parent directory of 

659 """ 

660 parent = Path(path).parent 

661 if not parent.is_dir(): 

662 raise OSError(rf"Cannot save file into a non-existent directory: '{parent}'") 

663 

664 

665@overload 

666def get_handle( 

667 path_or_buf: FilePath | BaseBuffer, 

668 mode: str, 

669 *, 

670 encoding: str | None = ..., 

671 compression: CompressionOptions = ..., 

672 memory_map: bool = ..., 

673 is_text: Literal[False], 

674 errors: str | None = ..., 

675 storage_options: StorageOptions = ..., 

676) -> IOHandles[bytes]: ... 

677 

678 

679@overload 

680def get_handle( 

681 path_or_buf: FilePath | BaseBuffer, 

682 mode: str, 

683 *, 

684 encoding: str | None = ..., 

685 compression: CompressionOptions = ..., 

686 memory_map: bool = ..., 

687 is_text: Literal[True] = ..., 

688 errors: str | None = ..., 

689 storage_options: StorageOptions = ..., 

690) -> IOHandles[str]: ... 

691 

692 

693@overload 

694def get_handle( 

695 path_or_buf: FilePath | BaseBuffer, 

696 mode: str, 

697 *, 

698 encoding: str | None = ..., 

699 compression: CompressionOptions = ..., 

700 memory_map: bool = ..., 

701 is_text: bool = ..., 

702 errors: str | None = ..., 

703 storage_options: StorageOptions = ..., 

704) -> IOHandles[str] | IOHandles[bytes]: ... 

705 

706 

707def get_handle( 

708 path_or_buf: FilePath | BaseBuffer, 

709 mode: str, 

710 *, 

711 encoding: str | None = None, 

712 compression: CompressionOptions | None = None, 

713 memory_map: bool = False, 

714 is_text: bool = True, 

715 errors: str | None = None, 

716 storage_options: StorageOptions | None = None, 

717) -> IOHandles[str] | IOHandles[bytes]: 

718 """ 

719 Get file handle for given path/buffer and mode. 

720 

721 Parameters 

722 ---------- 

723 path_or_buf : str or file handle 

724 File path or object. 

725 mode : str 

726 Mode to open path_or_buf with. 

727 encoding : str or None 

728 Encoding to use. 

729 compression : str or dict, default 'infer' 

730 For on-the-fly compression of the output data. If 'infer' and 'path_or_buf' 

731 is path-like, then detect compression from the following extensions: '.gz', 

732 '.bz2', '.zip', '.xz', '.zst', '.tar', '.tar.gz', '.tar.xz' or '.tar.bz2' 

733 (otherwise no compression). 

734 Set to ``None`` for no compression. 

735 Can also be a dict with key ``'method'`` set 

736 to one of {``'zip'``, ``'gzip'``, ``'bz2'``, ``'zstd'``, ``'xz'``, ``'tar'``} 

737 and other key-value pairs are forwarded to 

738 ``zipfile.ZipFile``, ``gzip.GzipFile``, 

739 ``bz2.BZ2File``, ``zstandard.ZstdCompressor``, ``lzma.LZMAFile`` or 

740 ``tarfile.TarFile``, respectively. 

741 As an example, the following could be passed for faster compression and to 

742 create a reproducible gzip archive: 

743 ``compression={'method': 'gzip', 'compresslevel': 1, 'mtime': 1}``. 

744 

745 May be a dict with key 'method' as compression mode 

746 and other keys as compression options if compression 

747 mode is 'zip'. 

748 

749 Passing compression options as keys in dict is 

750 supported for compression modes 'gzip', 'bz2', 'zstd' and 'zip'. 

751 

752 memory_map : bool, default False 

753 See parsers._parser_params for more information. Only used by read_csv. 

754 is_text : bool, default True 

755 Whether the type of the content passed to the file/buffer is string or 

756 bytes. This is not the same as `"b" not in mode`. If a string content is 

757 passed to a binary file/buffer, a wrapper is inserted. 

758 errors : str, default 'strict' 

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

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

761 of options. 

762 storage_options: StorageOptions = None 

763 Passed to _get_filepath_or_buffer 

764 

765 Returns the dataclass IOHandles 

766 """ 

767 # Windows does not default to utf-8. Set to utf-8 for a consistent behavior 

768 encoding = encoding or "utf-8" 

769 

770 errors = errors or "strict" 

771 

772 # read_csv does not know whether the buffer is opened in binary/text mode 

773 if _is_binary_mode(path_or_buf, mode) and "b" not in mode: 

774 mode += "b" 

775 

776 # validate encoding and errors 

777 codecs.lookup(encoding) 

778 if isinstance(errors, str): 

779 codecs.lookup_error(errors) 

780 

781 # open URLs 

782 ioargs = _get_filepath_or_buffer( 

783 path_or_buf, 

784 encoding=encoding, 

785 compression=compression, 

786 mode=mode, 

787 storage_options=storage_options, 

788 ) 

789 

790 handle = ioargs.filepath_or_buffer 

791 handles: list[BaseBuffer] = [] 

792 completed = False 

793 

794 try: 

795 # memory mapping needs to be the first step 

796 # only used for read_csv 

797 handle, memory_map, handles = _maybe_memory_map(handle, memory_map) 

798 

799 is_path = isinstance(handle, str) 

800 compression_args = dict(ioargs.compression) 

801 compression = compression_args.pop("method") 

802 

803 # Only for write methods 

804 if "r" not in mode and is_path: 

805 check_parent_directory(str(handle)) 

806 

807 if compression: 

808 if compression != "zstd": 

809 # compression libraries do not like an explicit text-mode 

810 ioargs.mode = ioargs.mode.replace("t", "") 

811 elif compression == "zstd" and "b" not in ioargs.mode: 

812 # python-zstandard defaults to text mode, but we always expect 

813 # compression libraries to use binary mode. 

814 ioargs.mode += "b" 

815 

816 # GZ Compression 

817 if compression == "gzip": 

818 if isinstance(handle, str): 

819 handle = gzip.GzipFile( 

820 filename=handle, 

821 mode=ioargs.mode, 

822 **compression_args, 

823 ) 

824 else: 

825 handle = gzip.GzipFile( 

826 # No overload variant of "GzipFile" matches argument types 

827 # "Union[str, BaseBuffer]", "str", "Dict[str, Any]" 

828 fileobj=handle, # type: ignore[call-overload] 

829 mode=ioargs.mode, 

830 **compression_args, 

831 ) 

832 

833 # BZ Compression 

834 elif compression == "bz2": 

835 import bz2 

836 

837 # Overload of "BZ2File" to handle pickle protocol 5 

838 # "Union[str, BaseBuffer]", "str", "Dict[str, Any]" 

839 handle = bz2.BZ2File( # type: ignore[call-overload] 

840 handle, 

841 mode=ioargs.mode, 

842 **compression_args, 

843 ) 

844 

845 # ZIP Compression 

846 elif compression == "zip": 

847 # error: Argument 1 to "_BytesZipFile" has incompatible type 

848 # "Union[str, BaseBuffer]"; expected "Union[Union[str, PathLike[str]], 

849 # ReadBuffer[bytes], WriteBuffer[bytes]]" 

850 handle = _BytesZipFile( 

851 handle, # type: ignore[arg-type] 

852 ioargs.mode, 

853 **compression_args, 

854 ) 

855 if handle.buffer.mode == "r": 

856 handles.append(handle) 

857 zip_names = handle.buffer.namelist() 

858 if len(zip_names) == 1: 

859 # Read the entire zip entry into memory rather than 

860 # returning a streaming ZipExtFile via .open(). On 

861 # Python <3.12, ZipExtFile has poor buffering that 

862 # causes O(n²) performance with pickle.load() and 

863 # other consumers that issue many small reads 

864 # (GH#59279). Can be reverted to .open() once the 

865 # minimum Python version is 3.12. 

866 handle = BytesIO(handle.buffer.read(zip_names.pop())) 

867 elif not zip_names: 

868 raise ValueError(f"Zero files found in ZIP file {path_or_buf}") 

869 else: 

870 raise ValueError( 

871 "Multiple files found in ZIP file. " 

872 f"Only one file per ZIP: {zip_names}" 

873 ) 

874 

875 # TAR Encoding 

876 elif compression == "tar": 

877 compression_args.setdefault("mode", ioargs.mode) 

878 if isinstance(handle, str): 

879 handle = _BytesTarFile(name=handle, **compression_args) 

880 else: 

881 # error: Argument "fileobj" to "_BytesTarFile" has incompatible 

882 # type "BaseBuffer"; expected "Union[ReadBuffer[bytes], 

883 # WriteBuffer[bytes], None]" 

884 handle = _BytesTarFile( 

885 fileobj=handle, # type: ignore[arg-type] 

886 **compression_args, 

887 ) 

888 assert isinstance(handle, _BytesTarFile) 

889 if "r" in handle.buffer.mode: 

890 handles.append(handle) 

891 files = handle.buffer.getnames() 

892 if len(files) == 1: 

893 file = handle.buffer.extractfile(files[0]) 

894 assert file is not None 

895 handle = file 

896 elif not files: 

897 raise ValueError( 

898 f"Zero files found in TAR archive {path_or_buf}" 

899 ) 

900 else: 

901 raise ValueError( 

902 "Multiple files found in TAR archive. " 

903 f"Only one file per TAR archive: {files}" 

904 ) 

905 

906 # XZ Compression 

907 elif compression == "xz": 

908 # error: Argument 1 to "LZMAFile" has incompatible type 

909 # "Union[str, BaseBuffer]"; expected "Optional[Union[Union[str, 

910 # bytes, PathLike[str], PathLike[bytes]], IO[bytes]], None]" 

911 import lzma 

912 

913 handle = lzma.LZMAFile( 

914 handle, # type: ignore[arg-type] 

915 ioargs.mode, 

916 **compression_args, 

917 ) 

918 

919 # Zstd Compression 

920 elif compression == "zstd": 

921 zstd = import_optional_dependency("zstandard") 

922 if "r" in ioargs.mode: 

923 open_args = {"dctx": zstd.ZstdDecompressor(**compression_args)} 

924 else: 

925 open_args = {"cctx": zstd.ZstdCompressor(**compression_args)} 

926 handle = zstd.open( 

927 handle, 

928 mode=ioargs.mode, 

929 **open_args, 

930 ) 

931 

932 # Unrecognized Compression 

933 else: 

934 msg = f"Unrecognized compression type: {compression}" 

935 raise ValueError(msg) 

936 

937 assert not isinstance(handle, str) 

938 handles.append(handle) 

939 

940 elif isinstance(handle, str): 

941 # Check whether the filename is to be opened in binary mode. 

942 # Binary mode does not support 'encoding' and 'newline'. 

943 if ioargs.encoding and "b" not in ioargs.mode: 

944 # Encoding 

945 handle = open( 

946 handle, 

947 ioargs.mode, 

948 encoding=ioargs.encoding, 

949 errors=errors, 

950 newline="", 

951 ) 

952 else: 

953 # Binary mode 

954 handle = open(handle, ioargs.mode) 

955 handles.append(handle) 

956 

957 # Convert BytesIO or file objects passed with an encoding 

958 is_wrapped = False 

959 if not is_text and ioargs.mode == "rb" and isinstance(handle, TextIOBase): 

960 # not added to handles as it does not open/buffer resources 

961 handle = _BytesIOWrapper( 

962 handle, 

963 encoding=ioargs.encoding, 

964 errors=errors, 

965 ) 

966 elif is_text and ( 

967 compression or memory_map or _is_binary_mode(handle, ioargs.mode) 

968 ): 

969 if ( 

970 not hasattr(handle, "readable") 

971 or not hasattr(handle, "writable") 

972 or not hasattr(handle, "seekable") 

973 ): 

974 handle = _IOWrapper(handle) 

975 # error: Value of type variable "_BufferT_co" of "TextIOWrapper" cannot 

976 # be "_IOWrapper | BaseBuffer" [type-var] 

977 handle = TextIOWrapper( 

978 handle, # type: ignore[type-var] 

979 encoding=ioargs.encoding, 

980 errors=errors, 

981 newline="", 

982 ) 

983 handles.append(handle) 

984 # only marked as wrapped when the caller provided a handle 

985 is_wrapped = not ( 

986 isinstance(ioargs.filepath_or_buffer, str) or ioargs.should_close 

987 ) 

988 

989 if "r" in ioargs.mode and not hasattr(handle, "read"): 

990 raise TypeError( 

991 "Expected file path name or file-like object, " 

992 f"got {type(ioargs.filepath_or_buffer)} type" 

993 ) 

994 

995 completed = True 

996 finally: 

997 if not completed: 

998 # Do not leak the handles that were opened before the failure; the 

999 # caller never receives an IOHandles to close them with (GH#58131) 

1000 for created_handle in reversed(handles): 

1001 created_handle.close() # type: ignore[attr-defined] 

1002 if ioargs.should_close: 

1003 assert not isinstance(ioargs.filepath_or_buffer, str) 

1004 ioargs.filepath_or_buffer.close() # type: ignore[attr-defined] 

1005 for close_handle in ioargs.close_handles: 

1006 close_handle.close() 

1007 

1008 handles.reverse() # close the most recently added buffer first 

1009 if ioargs.should_close: 

1010 assert not isinstance(ioargs.filepath_or_buffer, str) 

1011 handles.append(ioargs.filepath_or_buffer) 

1012 handles.extend(ioargs.close_handles) 

1013 

1014 return IOHandles( 

1015 # error: Argument "handle" to "IOHandles" has incompatible type 

1016 # "Union[TextIOWrapper, GzipFile, BaseBuffer, typing.IO[bytes], 

1017 # typing.IO[Any]]"; expected "pandas._typing.IO[Any]" 

1018 handle=handle, # type: ignore[arg-type] 

1019 # error: Argument "created_handles" to "IOHandles" has incompatible type 

1020 # "List[BaseBuffer]"; expected "List[Union[IO[bytes], IO[str]]]" 

1021 created_handles=handles, # type: ignore[arg-type] 

1022 is_wrapped=is_wrapped, 

1023 compression=ioargs.compression, 

1024 ) 

1025 

1026 

1027class _BufferedWriter(BytesIO, ABC): 

1028 """ 

1029 Some objects do not support multiple .write() calls (TarFile and ZipFile). 

1030 This wrapper writes to the underlying buffer on close. 

1031 """ 

1032 

1033 buffer = BytesIO() 

1034 

1035 @abstractmethod 

1036 def write_to_buffer(self) -> None: ... 

1037 

1038 def close(self) -> None: 

1039 if self.closed: 

1040 # already closed 

1041 return 

1042 if self.getbuffer().nbytes: 

1043 # write to buffer 

1044 self.seek(0) 

1045 with self.buffer: 

1046 self.write_to_buffer() 

1047 else: 

1048 self.buffer.close() 

1049 super().close() 

1050 

1051 

1052class _BytesTarFile(_BufferedWriter): 

1053 def __init__( 

1054 self, 

1055 name: str | None = None, 

1056 mode: Literal["r", "a", "w", "x"] = "r", 

1057 fileobj: ReadBuffer[bytes] | WriteBuffer[bytes] | None = None, 

1058 archive_name: str | None = None, 

1059 **kwargs: Any, 

1060 ) -> None: 

1061 super().__init__() 

1062 self.archive_name = archive_name 

1063 self.name = name 

1064 # error: No overload variant of "open" of "TarFile" matches argument 

1065 # types "str | None", "str", "ReadBuffer[bytes] | WriteBuffer[bytes] | None", 

1066 # "dict[str, Any]" 

1067 # error: Incompatible types in assignment (expression has type "TarFile", 

1068 # base class "_BufferedWriter" defined the type as "BytesIO") 

1069 self.buffer: tarfile.TarFile = tarfile.TarFile.open( # type: ignore[call-overload, assignment] 

1070 name=name, 

1071 mode=self.extend_mode(mode), 

1072 fileobj=fileobj, 

1073 **kwargs, 

1074 ) 

1075 

1076 def extend_mode(self, mode: str) -> str: 

1077 mode = mode.replace("b", "") 

1078 if mode != "w": 

1079 return mode 

1080 if self.name is not None: 

1081 suffix = Path(self.name).suffix 

1082 if suffix in (".gz", ".xz", ".bz2"): 

1083 mode = f"{mode}:{suffix[1:]}" 

1084 return mode 

1085 

1086 def infer_filename(self) -> str | None: 

1087 """ 

1088 If an explicit archive_name is not given, we still want the file inside the zip 

1089 file not to be named something.tar, because that causes confusion (GH39465). 

1090 """ 

1091 if self.name is None: 

1092 return None 

1093 

1094 filename = Path(self.name) 

1095 if filename.suffix == ".tar": 

1096 return filename.with_suffix("").name 

1097 elif filename.suffix in (".tar.gz", ".tar.bz2", ".tar.xz"): 

1098 return filename.with_suffix("").with_suffix("").name 

1099 return filename.name 

1100 

1101 def write_to_buffer(self) -> None: 

1102 # TarFile needs a non-empty string 

1103 archive_name = self.archive_name or self.infer_filename() or "tar" 

1104 tarinfo = tarfile.TarInfo(name=archive_name) 

1105 tarinfo.size = len(self.getvalue()) 

1106 self.buffer.addfile(tarinfo, self) 

1107 

1108 

1109class _BytesZipFile(_BufferedWriter): 

1110 def __init__( 

1111 self, 

1112 file: FilePath | ReadBuffer[bytes] | WriteBuffer[bytes], 

1113 mode: str, 

1114 archive_name: str | None = None, 

1115 **kwargs: Any, 

1116 ) -> None: 

1117 super().__init__() 

1118 mode = mode.replace("b", "") 

1119 self.archive_name = archive_name 

1120 

1121 kwargs.setdefault("compression", zipfile.ZIP_DEFLATED) 

1122 # error: No overload variant of "ZipFile" matches argument types 

1123 # "str | PathLike[str] | ReadBuffer[bytes] | WriteBuffer[bytes]", 

1124 # "str", "dict[str, Any]" 

1125 # error: Incompatible types in assignment (expression has type "ZipFile", 

1126 # base class "_BufferedWriter" defined the type as "BytesIO") 

1127 self.buffer: zipfile.ZipFile = zipfile.ZipFile( # type: ignore[call-overload, assignment] 

1128 file, mode, **kwargs 

1129 ) 

1130 

1131 def infer_filename(self) -> str | None: 

1132 """ 

1133 If an explicit archive_name is not given, we still want the file inside the zip 

1134 file not to be named something.zip, because that causes confusion (GH39465). 

1135 """ 

1136 if isinstance(self.buffer.filename, (os.PathLike, str)): 

1137 filename = Path(self.buffer.filename) 

1138 if filename.suffix == ".zip": 

1139 return filename.with_suffix("").name 

1140 return filename.name 

1141 return None 

1142 

1143 def write_to_buffer(self) -> None: 

1144 # ZipFile needs a non-empty string 

1145 archive_name = self.archive_name or self.infer_filename() or "zip" 

1146 self.buffer.writestr(archive_name, self.getvalue()) 

1147 

1148 

1149class _IOWrapper: 

1150 # TextIOWrapper is overly strict: it request that the buffer has seekable, readable, 

1151 # and writable. If we have a read-only buffer, we shouldn't need writable and vice 

1152 # versa. Some buffers, are seek/read/writ-able but they do not have the "-able" 

1153 # methods, e.g., tempfile.SpooledTemporaryFile. 

1154 # If a buffer does not have the above "-able" methods, we simply assume they are 

1155 # seek/read/writ-able. 

1156 def __init__(self, buffer: BaseBuffer) -> None: 

1157 self.buffer = buffer 

1158 

1159 def __getattr__(self, name: str) -> Any: 

1160 return getattr(self.buffer, name) 

1161 

1162 def readable(self) -> bool: 

1163 if hasattr(self.buffer, "readable"): 

1164 return self.buffer.readable() 

1165 return True 

1166 

1167 def seekable(self) -> bool: 

1168 if hasattr(self.buffer, "seekable"): 

1169 return self.buffer.seekable() 

1170 return True 

1171 

1172 def writable(self) -> bool: 

1173 if hasattr(self.buffer, "writable"): 

1174 return self.buffer.writable() 

1175 return True 

1176 

1177 

1178class _BytesIOWrapper: 

1179 # Wrapper that wraps a StringIO buffer and reads bytes from it 

1180 # Created for compat with pyarrow read_csv 

1181 def __init__( 

1182 self, 

1183 buffer: StringIO | TextIOBase, 

1184 encoding: str = "utf-8", 

1185 errors: str = "strict", 

1186 ) -> None: 

1187 self.buffer = buffer 

1188 self.encoding = encoding 

1189 self.errors = errors 

1190 # Because a character can be represented by more than 1 byte, 

1191 # it is possible that reading will produce more bytes than n 

1192 # We store the extra bytes in this overflow variable, and append the 

1193 # overflow to the front of the bytestring the next time reading is performed 

1194 self.overflow = b"" 

1195 

1196 def __getattr__(self, attr: str) -> Any: 

1197 return getattr(self.buffer, attr) 

1198 

1199 def read(self, n: int | None = -1) -> bytes: 

1200 assert self.buffer is not None 

1201 bytestring = self.buffer.read(n).encode(self.encoding, self.errors) 

1202 # When n=-1/n greater than remaining bytes: Read entire file/rest of file 

1203 combined_bytestring = self.overflow + bytestring 

1204 if n is None or n < 0 or n >= len(combined_bytestring): 

1205 self.overflow = b"" 

1206 return combined_bytestring 

1207 else: 

1208 to_return = combined_bytestring[:n] 

1209 self.overflow = combined_bytestring[n:] 

1210 return to_return 

1211 

1212 

1213def _maybe_memory_map( 

1214 handle: str | BaseBuffer, memory_map: bool 

1215) -> tuple[str | BaseBuffer, bool, list[BaseBuffer]]: 

1216 """Try to memory map file/buffer.""" 

1217 handles: list[BaseBuffer] = [] 

1218 memory_map &= hasattr(handle, "fileno") or isinstance(handle, str) 

1219 if not memory_map: 

1220 return handle, memory_map, handles 

1221 

1222 # mmap used by only read_csv 

1223 handle = cast("ReadCsvBuffer[bytes]", handle) 

1224 

1225 # need to open the file first 

1226 if isinstance(handle, str): 

1227 handle = open(handle, "rb") 

1228 handles.append(handle) 

1229 

1230 try: 

1231 # open mmap and adds *-able 

1232 # error: Argument 1 to "_IOWrapper" has incompatible type "mmap"; 

1233 # expected "BaseBuffer" 

1234 wrapped = _IOWrapper( 

1235 mmap.mmap( 

1236 handle.fileno(), 

1237 0, 

1238 access=mmap.ACCESS_READ, # type: ignore[arg-type] 

1239 ) 

1240 ) 

1241 except UnsupportedOperation as err: 

1242 # GH#45630 in-memory buffers like BytesIO/StringIO have a fileno 

1243 # method but raise UnsupportedOperation when it is called 

1244 raise ValueError( 

1245 "memory_map=True is only supported when reading from a file path " 

1246 "or a file-like object backed by a real file descriptor; " 

1247 "in-memory buffers (e.g. BytesIO, StringIO) are not supported." 

1248 ) from err 

1249 finally: 

1250 for handle in reversed(handles): 

1251 # error: "BaseBuffer" has no attribute "close" 

1252 handle.close() # type: ignore[attr-defined] 

1253 

1254 return wrapped, memory_map, [wrapped] 

1255 

1256 

1257def file_exists(filepath_or_buffer: FilePath | BaseBuffer) -> bool: 

1258 """Test whether file exists.""" 

1259 exists = False 

1260 filepath_or_buffer = stringify_path(filepath_or_buffer) 

1261 if not isinstance(filepath_or_buffer, str): 

1262 return exists 

1263 try: 

1264 exists = os.path.exists(filepath_or_buffer) 

1265 # gh-5874: if the filepath is too long will raise here 

1266 except (TypeError, ValueError): 

1267 pass 

1268 return exists 

1269 

1270 

1271def _is_binary_mode(handle: FilePath | BaseBuffer, mode: str) -> bool: 

1272 """Whether the handle is opened in binary mode""" 

1273 # specified by user 

1274 if "t" in mode or "b" in mode: 

1275 return "b" in mode 

1276 

1277 # exceptions 

1278 text_classes = ( 

1279 # classes that expect string but have 'b' in mode 

1280 codecs.StreamWriter, 

1281 codecs.StreamReader, 

1282 codecs.StreamReaderWriter, 

1283 TextIOBase, 

1284 ) 

1285 if issubclass(type(handle), text_classes): 

1286 return False 

1287 

1288 if isinstance(handle, _get_binary_io_classes()): 

1289 return True 

1290 

1291 if hasattr(handle, "mode"): 

1292 return "b" in handle.mode 

1293 

1294 # GH#52252 buffers without a "mode" attribute that are not covered by the 

1295 # classes above, e.g. mmap.mmap and botocore's StreamingBody, which is an 

1296 # IOBase subclass without being a Raw/BufferedIOBase subclass. 

1297 return isinstance(handle, (mmap.mmap, IOBase)) 

1298 

1299 

1300@functools.lru_cache 

1301def _get_binary_io_classes() -> tuple[type, ...]: 

1302 """IO classes that expect bytes""" 

1303 binary_classes: tuple[type, ...] = (BufferedIOBase, RawIOBase) 

1304 

1305 # python-zstandard doesn't use any of the builtin base classes; instead we 

1306 # have to use the `zstd.ZstdDecompressionReader` class for isinstance checks. 

1307 # Unfortunately `zstd.ZstdDecompressionReader` isn't exposed by python-zstandard 

1308 # so we have to get it from a `zstd.ZstdDecompressor` instance. 

1309 # See also https://github.com/indygreg/python-zstandard/pull/165. 

1310 zstd = import_optional_dependency("zstandard", errors="ignore") 

1311 if zstd is not None: 

1312 with zstd.ZstdDecompressor().stream_reader(b"") as reader: 

1313 binary_classes += (type(reader),) 

1314 

1315 return binary_classes 

1316 

1317 

1318def is_potential_multi_index( 

1319 columns: Sequence[Hashable] | MultiIndex, 

1320 index_col: bool | Sequence[int] | None = None, 

1321) -> bool: 

1322 """ 

1323 Check whether or not the `columns` parameter 

1324 could be converted into a MultiIndex. 

1325 

1326 Parameters 

1327 ---------- 

1328 columns : array-like 

1329 Object which may or may not be convertible into a MultiIndex 

1330 index_col : None, bool or list, optional 

1331 Column or columns to use as the (possibly hierarchical) index 

1332 

1333 Returns 

1334 ------- 

1335 bool : Whether or not columns could become a MultiIndex 

1336 """ 

1337 if index_col is None or isinstance(index_col, bool): 

1338 index_columns = set() 

1339 else: 

1340 index_columns = set(index_col) 

1341 

1342 return bool( 

1343 len(columns) 

1344 and not isinstance(columns, ABCMultiIndex) 

1345 and all(isinstance(c, tuple) for c in columns if c not in index_columns) 

1346 ) 

1347 

1348 

1349def dedup_names( 

1350 names: Sequence[Hashable], is_potential_multiindex: bool 

1351) -> Sequence[Hashable]: 

1352 """ 

1353 Rename column names if duplicates exist. 

1354 

1355 Currently the renaming is done by appending a period and an autonumeric, 

1356 but a custom pattern may be supported in the future. 

1357 

1358 Examples 

1359 -------- 

1360 >>> dedup_names(["x", "y", "x", "x"], is_potential_multiindex=False) 

1361 ['x', 'y', 'x.1', 'x.2'] 

1362 """ 

1363 names = list(names) # so we can index 

1364 counts: defaultdict[Hashable, int] = defaultdict(int) 

1365 

1366 for i, col in enumerate(names): 

1367 cur_count = counts[col] 

1368 

1369 while cur_count > 0: 

1370 counts[col] = cur_count + 1 

1371 

1372 if is_potential_multiindex: 

1373 # for mypy 

1374 assert isinstance(col, tuple) 

1375 col = (*col[:-1], f"{col[-1]}.{cur_count}") 

1376 else: 

1377 col = f"{col}.{cur_count}" 

1378 cur_count = counts[col] 

1379 

1380 names[i] = col 

1381 counts[col] = cur_count + 1 

1382 

1383 return names 

1384 

1385 

1386def mangle_dupe_names( 

1387 names: Sequence[Hashable], 

1388 unnamed_col_indices: Sequence[int] = (), 

1389 dtype: DtypeArg | None = None, 

1390) -> list[Hashable]: 

1391 """ 

1392 De-duplicate column names, matching ``pandas._libs.parsers.TextReader``. 

1393 

1394 This is the header-mangling used by the read_csv engines, and it differs 

1395 from :func:`dedup_names` in three ways (see GH#50371): 

1396 

1397 - a mangled name that would collide with a name already present in 

1398 ``names`` is skipped, so ``["x", "x", "x.1"]`` becomes 

1399 ``["x", "x.2", "x.1"]`` rather than ``["x", "x.1", "x.1.1"]``; 

1400 - columns whose position is listed in ``unnamed_col_indices`` are mangled 

1401 only after the named columns, so the named columns keep their name; 

1402 - a dict ``dtype`` is updated in place so that a renamed column keeps the 

1403 dtype requested for its original name. 

1404 

1405 Examples 

1406 -------- 

1407 >>> mangle_dupe_names(["x", "x", "x.1"]) 

1408 ['x', 'x.2', 'x.1'] 

1409 """ 

1410 names = list(names) # so we can index 

1411 unnamed = set(unnamed_col_indices) 

1412 # Ensure that regular columns are used before unnamed ones 

1413 # to keep given names and mangle unnamed columns 

1414 col_loop_order = [i for i in range(len(names)) if i not in unnamed] + list( 

1415 unnamed_col_indices 

1416 ) 

1417 counts: dict[Hashable, int] = {} 

1418 

1419 for i in col_loop_order: 

1420 col = old_col = names[i] 

1421 cur_count = counts.get(col, 0) 

1422 

1423 if cur_count > 0: 

1424 while cur_count > 0: 

1425 counts[old_col] = cur_count + 1 

1426 col = f"{old_col}.{cur_count}" 

1427 if col in names: 

1428 cur_count += 1 

1429 else: 

1430 cur_count = counts.get(col, 0) 

1431 

1432 if ( 

1433 isinstance(dtype, dict) 

1434 and dtype.get(old_col) is not None 

1435 and dtype.get(col) is None 

1436 ): 

1437 dtype[col] = dtype[old_col] 

1438 

1439 names[i] = col 

1440 counts[col] = cur_count + 1 

1441 

1442 return names