6
6
from collections import abc
7
7
import dataclasses
8
8
import gzip
9
- import io
10
9
from io import (
11
10
BufferedIOBase ,
12
11
BytesIO ,
18
17
import mmap
19
18
import os
20
19
from pathlib import Path
21
- import tempfile
22
20
from typing import (
23
21
IO ,
24
22
Any ,
@@ -104,7 +102,7 @@ def close(self) -> None:
104
102
avoid closing the potentially user-created buffer.
105
103
"""
106
104
if self .is_wrapped :
107
- assert isinstance (self .handle , ( TextIOWrapper , BytesIOWrapper ) )
105
+ assert isinstance (self .handle , TextIOWrapper )
108
106
self .handle .flush ()
109
107
self .handle .detach ()
110
108
self .created_handles .remove (self .handle )
@@ -779,20 +777,17 @@ def get_handle(
779
777
# Convert BytesIO or file objects passed with an encoding
780
778
is_wrapped = False
781
779
if not is_text and ioargs .mode == "rb" and isinstance (handle , TextIOBase ):
782
- handle = BytesIOWrapper (
780
+ # not added to handles as it does not open/buffer resources
781
+ handle = _BytesIOWrapper (
783
782
handle ,
784
783
encoding = ioargs .encoding ,
785
784
)
786
- handles .append (handle )
787
- # the (text) handle is always provided by the caller
788
- # since get_handle would have opened it in binary mode
789
- is_wrapped = True
790
785
elif is_text and (compression or _is_binary_mode (handle , ioargs .mode )):
791
786
handle = TextIOWrapper (
792
787
# error: Argument 1 to "TextIOWrapper" has incompatible type
793
788
# "Union[IO[bytes], IO[Any], RawIOBase, BufferedIOBase, TextIOBase, mmap]";
794
789
# expected "IO[bytes]"
795
- handle , # type: ignore[arg-type]
790
+ _IOWrapper ( handle ) , # type: ignore[arg-type]
796
791
encoding = ioargs .encoding ,
797
792
errors = errors ,
798
793
newline = "" ,
@@ -935,7 +930,7 @@ def __init__(
935
930
self .decode = decode
936
931
937
932
self .attributes = {}
938
- for attribute in ("seekable" , "readable" , "writeable" ):
933
+ for attribute in ("seekable" , "readable" ):
939
934
if not hasattr (f , attribute ):
940
935
continue
941
936
self .attributes [attribute ] = getattr (f , attribute )()
@@ -976,11 +971,40 @@ def __next__(self) -> str:
976
971
return newline .lstrip ("\n " )
977
972
978
973
979
- # Wrapper that wraps a StringIO buffer and reads bytes from it
980
- # Created for compat with pyarrow read_csv
981
- class BytesIOWrapper (io .BytesIO ):
982
- buffer : StringIO | TextIOBase | None
974
+ class _IOWrapper :
975
+ # TextIOWrapper is overly strict: it request that the buffer has seekable, readable,
976
+ # and writable. If we have a read-only buffer, we shouldn't need writable and vice
977
+ # versa. Some buffers, are seek/read/writ-able but they do not have the "-able"
978
+ # methods, e.g., tempfile.SpooledTemporaryFile.
979
+ # If a buffer does not have the above "-able" methods, we simple assume they are
980
+ # seek/read/writ-able.
981
+ def __init__ (self , buffer : BaseBuffer ):
982
+ self .buffer = buffer
983
+
984
+ def __getattr__ (self , name : str ):
985
+ return getattr (self .buffer , name )
986
+
987
+ def readable (self ) -> bool :
988
+ if hasattr (self .buffer , "readable" ):
989
+ # error: "BaseBuffer" has no attribute "readable"
990
+ return self .buffer .readable () # type: ignore[attr-defined]
991
+ return True
992
+
993
+ def seekable (self ) -> bool :
994
+ if hasattr (self .buffer , "seekable" ):
995
+ return self .buffer .seekable ()
996
+ return True
997
+
998
+ def writable (self ) -> bool :
999
+ if hasattr (self .buffer , "writable" ):
1000
+ # error: "BaseBuffer" has no attribute "writable"
1001
+ return self .buffer .writable () # type: ignore[attr-defined]
1002
+ return True
983
1003
1004
+
1005
+ class _BytesIOWrapper :
1006
+ # Wrapper that wraps a StringIO buffer and reads bytes from it
1007
+ # Created for compat with pyarrow read_csv
984
1008
def __init__ (self , buffer : StringIO | TextIOBase , encoding : str = "utf-8" ):
985
1009
self .buffer = buffer
986
1010
self .encoding = encoding
@@ -1006,15 +1030,6 @@ def read(self, n: int | None = -1) -> bytes:
1006
1030
self .overflow = combined_bytestring [n :]
1007
1031
return to_return
1008
1032
1009
- def detach (self ):
1010
- # Slightly modified from Python's TextIOWrapper detach method
1011
- if self .buffer is None :
1012
- raise ValueError ("buffer is already detached" )
1013
- self .flush ()
1014
- buffer = self .buffer
1015
- self .buffer = None
1016
- return buffer
1017
-
1018
1033
1019
1034
def _maybe_memory_map (
1020
1035
handle : str | BaseBuffer ,
@@ -1042,10 +1057,15 @@ def _maybe_memory_map(
1042
1057
1043
1058
# error: Argument 1 to "_MMapWrapper" has incompatible type "Union[IO[Any],
1044
1059
# RawIOBase, BufferedIOBase, TextIOBase, mmap]"; expected "IO[Any]"
1045
- wrapped = cast (
1046
- BaseBuffer ,
1047
- _MMapWrapper (handle , encoding , errors , decode ), # type: ignore[arg-type]
1048
- )
1060
+ try :
1061
+ wrapped = cast (
1062
+ BaseBuffer ,
1063
+ _MMapWrapper (handle , encoding , errors , decode ), # type: ignore[arg-type]
1064
+ )
1065
+ finally :
1066
+ for handle in reversed (handles ):
1067
+ # error: "BaseBuffer" has no attribute "close"
1068
+ handle .close () # type: ignore[attr-defined]
1049
1069
handles .append (wrapped )
1050
1070
1051
1071
return wrapped , memory_map , handles
@@ -1077,8 +1097,6 @@ def _is_binary_mode(handle: FilePath | BaseBuffer, mode: str) -> bool:
1077
1097
codecs .StreamWriter ,
1078
1098
codecs .StreamReader ,
1079
1099
codecs .StreamReaderWriter ,
1080
- # cannot be wrapped in TextIOWrapper GH43439
1081
- tempfile .SpooledTemporaryFile ,
1082
1100
)
1083
1101
if issubclass (type (handle ), text_classes ):
1084
1102
return False
0 commit comments