Skip to content

Commit 154288f

Browse files
authored
Convert FileFormatModel from ABC to typing.Protocol (#3692)
Closes #3100 ## Rationale for this change `FileFormatModel` had only abstract methods (no shared implementation), so an `ABC` was heavier than needed. This PR converts it to `typing.Protocol`, which is the more Pythonic pattern for a pure interface. `FileFormatWriter` remains an `ABC` since it has shared `__enter__`/`__exit__`/`result` implementations that inheritance carries for free. `FileFormatFactory` is unchanged - it matches the existing `AuthManagerFactory` pattern. ## Changes New format implementations can now conform structurally without inheriting from `FileFormatModel`. Net: 2 `ABC`s → 1 `ABC` + 1 `Protocol`. No signature change for existing callers; `ParquetFormatModel(FileFormatModel)` inheritance is retained (`Protocol` supports explicit conformance). ## Are these changes tested? Yes. `tests/io/test_fileformat.py` adds two tests using a shared `_StructuralModel` helper: - `test_file_format_model_is_protocol` - a structurally-conforming class (no inheritance) passes `isinstance()` against `FileFormatModel`. - `test_structural_model_works_with_factory` - a structurally-conforming class can be registered and retrieved via `FileFormatFactory` end-to-end. ## Are there any user-facing changes? No. Default behavior is unchanged.
1 parent 23aa9a9 commit 154288f

2 files changed

Lines changed: 38 additions & 6 deletions

File tree

‎pyiceberg/io/fileformat.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from abc import ABC, abstractmethod
2323
from dataclasses import dataclass
24-
from typing import TYPE_CHECKING, Any
24+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
2525

2626
from pyiceberg.io import OutputFile
2727
from pyiceberg.manifest import FileFormat
@@ -143,28 +143,27 @@ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
143143
self._result = self.close()
144144

145145

146-
class FileFormatModel(ABC):
146+
@runtime_checkable
147+
class FileFormatModel(Protocol):
147148
"""Represents a file format's capabilities. Creates writers."""
148149

149150
@property
150-
@abstractmethod
151151
def format(self) -> FileFormat: ...
152152

153-
@abstractmethod
154153
def file_extension(self) -> str:
155154
"""Return file extension without dot, e.g. 'parquet', 'orc'."""
155+
...
156156

157-
@abstractmethod
158157
def create_writer(
159158
self,
160159
output_file: OutputFile,
161160
file_schema: Schema,
162161
properties: Properties,
163162
) -> FileFormatWriter: ...
164163

165-
@abstractmethod
166164
def add_field_metadata(self, field: NestedField, metadata: dict[bytes, bytes], include_field_ids: bool) -> None:
167165
"""Add format-specific Arrow field metadata."""
166+
...
168167

169168

170169
class FileFormatFactory:

‎tests/io/test_fileformat.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,36 @@ def close(self) -> DataFileStatistics:
7777
writer = _DummyWriter()
7878
with pytest.raises(RuntimeError, match="Writer has not been closed yet"):
7979
writer.result()
80+
81+
82+
class _StructuralModel:
83+
"""Non-inheriting class that structurally conforms to the FileFormatModel Protocol."""
84+
85+
@property
86+
def format(self) -> FileFormat:
87+
return FileFormat.ORC
88+
89+
def file_extension(self) -> str:
90+
return "orc"
91+
92+
def create_writer(self, output_file: Any, file_schema: Any, properties: Any) -> Any:
93+
raise NotImplementedError
94+
95+
def add_field_metadata(self, field: Any, metadata: Any, include_field_ids: bool) -> None:
96+
pass
97+
98+
99+
def test_file_format_model_is_protocol() -> None:
100+
"""A structurally-conforming class (no inheritance) passes isinstance() against FileFormatModel."""
101+
assert isinstance(_StructuralModel(), FileFormatModel)
102+
103+
104+
def test_structural_model_works_with_factory() -> None:
105+
"""A structurally-conforming class (no inheritance) can be registered and retrieved via FileFormatFactory."""
106+
original = dict(FileFormatFactory._registry)
107+
try:
108+
model = _StructuralModel()
109+
FileFormatFactory.register(model)
110+
assert FileFormatFactory.get(FileFormat.ORC) is model
111+
finally:
112+
FileFormatFactory._registry = original

0 commit comments

Comments
 (0)