11from pathlib import Path
2- from typing import IO , Literal , Type
2+ from typing import IO , Literal , overload
33import io
4+ import os
45
56import fileex
6- from fileex .typing import FileLike
7+ from fileex .typing import FileLike , ReadableFileLike
78
89
910def open_file (
@@ -57,11 +58,33 @@ def open_file(
5758 )
5859
5960
61+ @overload
62+ def content (
63+ file : FileLike ,
64+ * ,
65+ output : Literal ["str" ] = "str" ,
66+ encoding : str = "utf-8" ,
67+ errors : str = "strict" ,
68+ preserve_pos : bool = True ,
69+ ) -> str : ...
70+
71+ @overload
72+ def content (
73+ file : FileLike ,
74+ * ,
75+ output : Literal ["bytes" ],
76+ encoding : str = "utf-8" ,
77+ errors : str = "strict" ,
78+ preserve_pos : bool = True ,
79+ ) -> bytes : ...
80+
6081def content (
6182 file : FileLike ,
6283 * ,
6384 output : Literal ["str" , "bytes" ] = "str" ,
64- encoding : str = "utf-8"
85+ encoding : str = "utf-8" ,
86+ errors : str = "strict" ,
87+ preserve_pos : bool = True ,
6588) -> str | bytes :
6689 """Get the content of a file-like input.
6790
@@ -72,30 +95,78 @@ def content(
7295 output
7396 Output type, either 'str' or 'bytes'.
7497 encoding
75- Encoding used to decode the file if it is provided as bytes or Path,
76- and output is 'str'.
98+ Encoding used when converting between bytes and str.
99+ errors
100+ Error handling for encode/decode (e.g. 'strict', 'replace', 'ignore').
101+ preserve_pos
102+ If True and the object is seekable, restores the stream position after reading.
77103
78104 Returns
79105 -------
80106 file_content
81107 Content of the file as a string or bytes.
108+
109+ Raises
110+ -------
111+ ValueError
112+ If `output` is not 'str' or 'bytes'.
113+ TypeError
114+ If `file` is not a supported type.
82115 """
116+ def return_from_bytes (b : bytes ) -> str | bytes :
117+ """Helper to return bytes or decoded str based on output parameter."""
118+ return b .decode (encoding , errors = errors ) if output == "str" else b
119+
120+ def return_from_str (s : str ) -> str | bytes :
121+ """Helper to return str or encoded bytes based on output parameter."""
122+ return s if output == "str" else s .encode (encoding , errors = errors )
123+
83124 if output not in ("str" , "bytes" ):
84125 raise ValueError ("output must be either 'str' or 'bytes'" )
85126
127+ # Path: normalize to bytes via filesystem read
128+ if isinstance (file , os .PathLike ):
129+ return return_from_bytes (Path (file ).read_bytes ())
130+
131+ # String: check if path or content
86132 if isinstance (file , str ):
87- content_bytes = (
88- Path (file ).read_bytes ()
89- if fileex .path .is_path (file ) else
90- file .encode (encoding )
91- )
92- elif isinstance (file , bytes ):
93- content_bytes = file
94- elif isinstance (file , Path ):
95- content_bytes = file .read_bytes ()
96- else :
133+ if fileex .path .is_path (file ):
134+ return return_from_bytes (Path (file ).read_bytes ())
135+ return return_from_str (file )
136+
137+ # Bytes-like: normalize to bytes
138+ if isinstance (file , (bytes , bytearray , memoryview )):
139+ return return_from_bytes (bytes (file ))
140+
141+ # Streams / file objects (open(...), BytesIO, sockets, etc.)
142+ if isinstance (file , ReadableFileLike ):
143+ # Try to preserve cursor position if possible.
144+ pos : int | None = None
145+ if preserve_pos :
146+ try :
147+ pos = file .tell () # type: ignore[attr-defined]
148+ except Exception :
149+ pos = None
150+
151+ try :
152+ raw = file .read ()
153+ finally :
154+ if preserve_pos and pos is not None :
155+ try :
156+ file .seek (pos ) # type: ignore[attr-defined]
157+ except Exception :
158+ pass
159+
160+ if isinstance (raw , str ):
161+ return return_from_str (raw )
162+ if isinstance (raw , (bytes , bytearray , memoryview )):
163+ return return_from_bytes (bytes (raw ))
164+
97165 raise TypeError (
98- f"Expected str, bytes, or pathlib.Path , got { type (file ).__name__ } "
166+ f"file.read() must return str or bytes , got { type (raw ).__name__ } "
99167 )
100168
101- return content_bytes .decode (encoding ) if output == "str" else content_bytes
169+ raise TypeError (
170+ "Expected str, bytes-like, path-like, or a readable stream; "
171+ f"got { type (file ).__name__ } "
172+ )
0 commit comments