@@ -72,6 +72,9 @@ class IOArgs:
72
72
compression : CompressionDict
73
73
should_close : bool = False
74
74
75
+ def __post_init__ (self ):
76
+ self .parent_exists = dir_exists (self .filepath_or_buffer )
77
+
75
78
76
79
@dataclasses .dataclass
77
80
class IOHandles :
@@ -630,6 +633,17 @@ def get_handle(
630
633
compression_args = dict (ioargs .compression )
631
634
compression = compression_args .pop ("method" )
632
635
636
+ # If the parent directory doesn't exist initializing the stream will fail (GH 24306)
637
+ if (
638
+ _is_writable_mode (mode )
639
+ and is_path
640
+ and not ioargs .parent_exists
641
+ ):
642
+ os .makedirs (
643
+ os .path .dirname (ioargs .filepath_or_buffer ),
644
+ exist_ok = True ,
645
+ )
646
+
633
647
if compression :
634
648
# compression libraries do not like an explicit text-mode
635
649
ioargs .mode = ioargs .mode .replace ("t" , "" )
@@ -937,6 +951,20 @@ def file_exists(filepath_or_buffer: FilePathOrBuffer) -> bool:
937
951
return exists
938
952
939
953
954
+ def dir_exists (filepath_or_buffer : FilePathOrBuffer ) -> bool :
955
+ """Test whether parent directory exists."""
956
+ exists = False
957
+ filepath_or_buffer = stringify_path (filepath_or_buffer )
958
+ if not isinstance (filepath_or_buffer , str ):
959
+ return exists
960
+ try :
961
+ exists = os .path .exists (os .path .dirname (filepath_or_buffer ))
962
+ # gh-5874: if the filepath is too long will raise here
963
+ except (TypeError , ValueError ):
964
+ pass
965
+ return exists
966
+
967
+
940
968
def _is_binary_mode (handle : FilePathOrBuffer , mode : str ) -> bool :
941
969
"""Whether the handle is opened in binary mode"""
942
970
# specified by user
@@ -951,3 +979,11 @@ def _is_binary_mode(handle: FilePathOrBuffer, mode: str) -> bool:
951
979
# classes that expect bytes
952
980
binary_classes = (BufferedIOBase , RawIOBase )
953
981
return isinstance (handle , binary_classes ) or "b" in getattr (handle , "mode" , mode )
982
+
983
+
984
+ def _is_writable_mode (mode : str ) -> bool :
985
+ """Whether the handle is opened in writable mode"""
986
+ writable_prefixes = ('a' , 'w' , 'r+' )
987
+ if any (map (mode .startswith , writable_prefixes )):
988
+ return True
989
+ return False
0 commit comments