From 1a79cd5373a50eece769ee45ca239bf3ecd926da Mon Sep 17 00:00:00 2001 From: Akash Kumar <116457960+akashchamp@users.noreply.github.com> Date: Sat, 12 Sep 2026 04:43:27 +0530 Subject: [PATCH] Core: Validate implementation classes loaded from properties --- pyiceberg/io/__init__.py | 2 ++ pyiceberg/io/pyarrow.py | 2 ++ pyiceberg/table/locations.py | 4 ++++ tests/io/test_io.py | 5 +++++ tests/io/test_pyarrow.py | 6 ++++++ tests/table/test_locations.py | 8 ++++++++ 6 files changed, 27 insertions(+) diff --git a/pyiceberg/io/__init__.py b/pyiceberg/io/__init__.py index c44e105e62..aff2db71ca 100644 --- a/pyiceberg/io/__init__.py +++ b/pyiceberg/io/__init__.py @@ -340,6 +340,8 @@ def _import_file_io(io_impl: str, properties: Properties) -> FileIO | None: module_name, class_name = ".".join(path_parts[:-1]), path_parts[-1] module = importlib.import_module(module_name) class_ = getattr(module, class_name) + if not isinstance(class_, type) or not issubclass(class_, FileIO): + raise ValueError(f"py-io-impl should be a subclass of FileIO, got: {io_impl}") return class_(properties) except ModuleNotFoundError: logger.warning(f"Could not initialize FileIO: {io_impl}", exc_info=logger.isEnabledFor(logging.DEBUG)) diff --git a/pyiceberg/io/pyarrow.py b/pyiceberg/io/pyarrow.py index c36f1639d9..09699d44b8 100644 --- a/pyiceberg/io/pyarrow.py +++ b/pyiceberg/io/pyarrow.py @@ -232,6 +232,8 @@ def _import_retry_strategy(impl: str) -> S3RetryStrategy | None: module_name, class_name = ".".join(path_parts[:-1]), path_parts[-1] module = importlib.import_module(module_name) class_ = getattr(module, class_name) + if not isinstance(class_, type) or not issubclass(class_, S3RetryStrategy): + raise ValueError(f"retry-strategy-impl should be a subclass of S3RetryStrategy, got: {impl}") return class_() except (ModuleNotFoundError, AttributeError): warnings.warn(f"Could not initialize S3 retry strategy: {impl}", stacklevel=2) diff --git a/pyiceberg/table/locations.py b/pyiceberg/table/locations.py index 771c6b5a0f..279cd0ddfd 100644 --- a/pyiceberg/table/locations.py +++ b/pyiceberg/table/locations.py @@ -178,6 +178,10 @@ def _import_location_provider( module_name, class_name = ".".join(path_parts[:-1]), path_parts[-1] module = importlib.import_module(module_name) class_ = getattr(module, class_name) + if not isinstance(class_, type) or not issubclass(class_, LocationProvider): + raise ValueError( + f"write.py-location-provider.impl should be a subclass of LocationProvider, got: {location_provider_impl}" + ) return class_(table_location, table_properties) except ModuleNotFoundError: logger.warning( diff --git a/tests/io/test_io.py b/tests/io/test_io.py index 51aa86c67f..72bdc2ffa9 100644 --- a/tests/io/test_io.py +++ b/tests/io/test_io.py @@ -280,6 +280,11 @@ def test_import_file_io() -> None: assert isinstance(_import_file_io(ARROW_FILE_IO, {}), PyArrowFileIO) +def test_import_file_io_wrong_type() -> None: + with pytest.raises(ValueError, match="py-io-impl should be a subclass of FileIO"): + _import_file_io("pyiceberg.table.locations.SimpleLocationProvider", {}) + + def test_import_file_io_does_not_exist(caplog: Any) -> None: import logging diff --git a/tests/io/test_pyarrow.py b/tests/io/test_pyarrow.py index b31c18949b..8a671f3d34 100644 --- a/tests/io/test_pyarrow.py +++ b/tests/io/test_pyarrow.py @@ -3309,6 +3309,12 @@ def test_retry_strategy() -> None: io.new_input("s3://bucket/path/to/file") +def test_retry_strategy_wrong_type() -> None: + io = PyArrowFileIO(properties={S3_RETRY_STRATEGY_IMPL: "pyiceberg.io.FileIO"}) + with pytest.raises(ValueError, match="retry-strategy-impl should be a subclass of S3RetryStrategy"): + io.new_input("s3://bucket/path/to/file") + + def test_retry_strategy_not_found() -> None: io = PyArrowFileIO(properties={S3_RETRY_STRATEGY_IMPL: "pyiceberg.DoesNotExist"}) with pytest.warns(UserWarning, match="Could not initialize S3 retry strategy: pyiceberg.DoesNotExist"): diff --git a/tests/table/test_locations.py b/tests/table/test_locations.py index 6dd7520f3c..dca0c00922 100644 --- a/tests/table/test_locations.py +++ b/tests/table/test_locations.py @@ -60,6 +60,14 @@ def test_custom_location_provider() -> None: assert provider.new_data_location("my_file") == "custom_location_provider/my_file" +def test_custom_location_provider_wrong_type() -> None: + with pytest.raises(ValueError, match="write.py-location-provider.impl should be a subclass of LocationProvider"): + load_location_provider( + table_location="table_location", + table_properties={"write.py-location-provider.impl": "pyiceberg.io.FileIO"}, + ) + + def test_custom_location_provider_single_path() -> None: with pytest.raises(ValueError, match=r"write\.py-location-provider\.impl should be full path"): load_location_provider(table_location="table_location", table_properties={"write.py-location-provider.impl": "not_found"})