Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Writer cleanup #6

Merged
merged 3 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions dataclass_io/_lib/file.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
from dataclasses import dataclass
from enum import Enum
from enum import unique
from io import TextIOWrapper
from typing import IO
from typing import Optional
from typing import TextIO
from typing import TypeAlias

ReadableFileHandle: TypeAlias = TextIOWrapper | IO | TextIO
WritableFileHandle: TypeAlias = TextIOWrapper | IO | TextIO


@unique
class WriteMode(Enum):
"""
The mode in which to open a file for writing.

Attributes:
value: The mode.
abbreviation: The short version of the mode (used with Python's `open()`).
"""

value: str
abbreviation: str

def __new__(cls, value: str, abbreviation: str) -> "WriteMode":
enum = object.__new__(cls)
enum._value_ = value

return enum

# NB: Specifying the additional fields in the `__init__` method instead of `__new__` is
# necessary in order to construct `WriteMode` from only the value (e.g. `WriteMode("append")`).
# Otherwise, `mypy` complains about a missing positional argument.
# https://stackoverflow.com/a/54732120
def __init__(self, _: str, abbreviation: str = None):
self.abbreviation = abbreviation

WRITE = "write", "w"
"""Write to a new file."""

APPEND = "append", "a"
"""Append to an existing file."""


@dataclass(frozen=True, kw_only=True)
Expand Down
56 changes: 11 additions & 45 deletions dataclass_io/writer.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,27 @@
from csv import DictWriter
from dataclasses import asdict
from enum import Enum
from enum import unique
from io import TextIOWrapper
from pathlib import Path
from types import TracebackType
from typing import IO
from typing import Any
from typing import Iterable
from typing import TextIO
from typing import Type
from typing import TypeAlias

from dataclass_io._lib.assertions import assert_dataclass_is_valid
from dataclass_io._lib.assertions import assert_fieldnames_are_dataclass_attributes
from dataclass_io._lib.assertions import assert_file_is_appendable
from dataclass_io._lib.assertions import assert_file_is_writable
from dataclass_io._lib.dataclass_extensions import DataclassInstance
from dataclass_io._lib.dataclass_extensions import fieldnames

WritableFileHandle: TypeAlias = TextIOWrapper | IO | TextIO


@unique
class WriteMode(Enum):
"""
The mode in which to open the file.

Attributes:
value: The mode.
abbreviation: The short version of the mode (used with Python's `open()`).
"""

value: str
abbreviation: str

def __new__(cls, value: str, abbreviation: str) -> "WriteMode":
enum = object.__new__(cls)
enum._value_ = value

return enum

# NB: Specifying the additional fields in the `__init__` method instead of `__new__` is
# necessary in order to construct `WriteMode` from only the value (e.g. `WriteMode("append")`).
# Otherwise, `mypy` complains about a missing positional argument.
# https://stackoverflow.com/a/54732120
def __init__(self, _: str, abbreviation: str = None):
self.abbreviation = abbreviation

WRITE = "write", "w"
"""Write to a new file."""

APPEND = "append", "a"
"""Append to an existing file."""
from dataclass_io._lib.file import WritableFileHandle
from dataclass_io._lib.file import WriteMode


class DataclassWriter:
_dataclass_type: type[DataclassInstance]
_fieldnames: list[str]
_fout: WritableFileHandle
_writer: DictWriter

def __init__(
self,
path: Path,
Expand Down Expand Up @@ -88,9 +54,9 @@ def __init__(
May not be used together with `include_fields`.

Raises:
FileNotFoundError: If the input file does not exist.
IsADirectoryError: If the input file path is a directory.
PermissionError: If the input file is not readable.
FileNotFoundError: If the output file does not exist when trying to append.
IsADirectoryError: If the output file path is a directory.
PermissionError: If the output file is not writable (or readable when trying to append).
TypeError: If the provided type is not a dataclass.
"""

Expand All @@ -101,6 +67,7 @@ def __init__(

assert_dataclass_is_valid(dataclass_type)

self._fieldnames: list[str]
if include_fields is not None and exclude_fields is not None:
raise ValueError(
"Only one of `include_fields` and `exclude_fields` may be specified, not both."
Expand All @@ -122,7 +89,6 @@ def __init__(

self._dataclass_type = dataclass_type
self._fout = path.open(write_mode.abbreviation)

self._writer = DictWriter(
f=self._fout,
fieldnames=self._fieldnames,
Expand Down