As pointed out by other answers, the second way is the better option. And I think that a FileContextHandle will not come in the future, because the csv-module estimates Iterator[str], not typing.IO.
However there is a lack for a csv_open, that provides full functionality and correct file-context-handling.
class csv_open:
"""
A in most cases useless class, but Stackoverflow threads have triggered me.
"""
def __init__(
self,
file: str | Path,
mode: str = "rt",
buffering: Optional[int] = 1,
encoding: Optional[str] = None,
errors: Optional[str] = None,
newline: Optional[typing.Literal["", "\n", "\r", "\n\r"]] = None,
):
"""
Notes:
if you do not specify a reader or writer a normal csv.reader is
created for mode r and a csv.writer is created for modes w,
a and x.
If you want dict-reader or dict-writer or open the file in
reading and writing mode you have to specify the reader or writer
explicitly.
Examples:
>>> with csv_open('names.csv', mode='rt') as reader:
... for row in reader:
... print(row)
>>> with csv_open("names.csv", mode="rt").dict_reader(
... fieldnames=['firstname', 'surname']) as reader:
... for row in reader:
... print(f"{row['firstname']} {row['surname']}")
Args:
mode: r-reading, w-writing, a-appending, x-create-write
buffering: 0 is forbidden, 1 is a per line buffering,
greater 1 is byte-block buffering.
encoding: any supported python encoding, defaults to archive
specific encoding, or system-specific if there is no archive
specific encoding.
errors: Indicates the error handling for encoding errors run
help(codecs.Codec) for more information.
newline: indicates if line endings should be converted
"""
self._newline = newline
self._errors = errors
self._encoding = encoding
self._buffering = buffering
self._mode = mode
self._file = path(file)
self._fd = None
self._reader = None
self._writer = None
def reader(self, dialect: str = "excel", **kwargs):
"""
indicates that a csv-reader should be created. This method is for
setting parameters explicit. The method is called implicitly if no
other reader or writer is set and file opening mode is `r`
Args:
dialect: the dialect that is used by the reader
kwargs: used to overwrite variables set in dialect
Returns:
Self
"""
self._reader = csv.reader(self._open(), dialect=dialect, **kwargs)
return self
def writer(self, dialect: str = "excel", **kwargs):
"""
indicates that a csv-writer should be createad. This method is for
setting parameters explicit. The method is called implicitly if no
other reader or writer is set and file opening mode is `w`, `a` or `x`
Args:
dialect: the dialect that is used by the writer
**kwargs: used to overwrite variables set in dialect
Returns:
Self
"""
self._writer = csv.writer(self._open(), dialect=dialect, **kwargs)
return self
def dict_reader(
self,
fieldnames: list[str],
restkey: typing.Any = None,
restval: typing.Any = None,
dialect: str = "excel",
**kwargs,
):
"""
Indicates that a csv-DictReader should be created. This method is
never called implicit.
Args:
fieldnames: the fieldnames of the dict, if None the first line
of the csv-file is used.
restkey: the key name for values that has no field-name mapping
restval: the value used for field-names without a value
dialect: the dialect that is used by the reader
**kwargs: used to overwrite dialect variables
Returns:
Self
"""
self._reader = csv.DictReader(
self._open(),
fieldnames=fieldnames,
restkey=restkey,
restval=restval,
dialect=dialect,
**kwargs,
)
return self
def dict_writer(
self,
fieldnames: list[str],
restval: typing.Any = None,
dialect: str = "excel",
extrasection: typing.Literal["raise", "ignore"] = "raise",
**kwargs,
) -> typing.Self:
"""
Indicates that a csv-DictWriter should be created. This method is
never called implicit.
Args:
fieldnames: the fieldnames of the dict, if None the first line of
the csv file is used.
restval: the value used for fieldnames that do not appear in the
dict.
dialect: the dialect that is used by the writer
extrasection: failure strategie dict-keys that are no fieldnames
**kwargs: used to overwrite variables set in dialect
Returns:
Self
"""
self._writer = csv.DictWriter(
self._open(),
fieldnames=fieldnames,
restval=restval,
extrasaction=extrasection,
dialect=dialect,
**kwargs,
)
return self
def _open(self):
if self._fd is not None:
raise ValueError("Only one reader or writer is allowed.")
self._fd = self._file.open(
mode=self._mode,
buffering=self._buffering,
encoding=self._encoding,
errors=self._errors,
newline=self._newline,
)
return self._fd
def __enter__(
self,
) -> csv.reader | csv.writer | csv.DictReader | csv.DictWriter:
if self._reader is not None:
return self._reader
elif self._writer is not None:
return self._writer
elif "w" in self._mode or "a" in self._mode or "x" in self._mode:
self.writer()
return self._writer
elif "r" in self._mode:
self.reader()
return self._reader
# handle any forgotten or invalid modes
raise RuntimeError("Please call a reader or writer constructor")
def __exit__(self, exc_type, exc_val, exc_tb):
self._fd.close()