Skip to content

Commit 1314fb4

Browse files
committed
IO: Fix Windows drive letters misidentified as URI schemes by urlparse
On Windows, Python's urlparse treats paths like 'C:\Users\...' as having scheme='c', which causes 'Unrecognized filesystem type in URI: c' errors. This adds a platform-guarded predicate that detects single-character alphabetic schemes on Windows and remaps them to 'file', enabling correct local filesystem routing. Fixes all three parse sites (_infer_file_io_from_scheme, PyArrowFileIO.parse_location, FsspecFileIO._get_fs_from_uri) and includes platform-conditional tests. Related: #2477, #1005
1 parent 154288f commit 1314fb4

5 files changed

Lines changed: 72 additions & 4 deletions

File tree

‎pyiceberg/io/__init__.py‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import importlib
2929
import logging
30+
import sys
3031
import warnings
3132
from abc import ABC, abstractmethod
3233
from io import SEEK_SET
@@ -41,6 +42,17 @@
4142

4243
logger = logging.getLogger(__name__)
4344

45+
46+
def _is_windows_drive_letter(scheme: str) -> bool:
47+
r"""Check if a parsed URL scheme is actually a Windows drive letter.
48+
49+
On Windows, Python's urlparse treats paths like 'C:\\Users\\...' as having
50+
scheme='c'. This detects that case so callers can route to the local filesystem.
51+
Only returns True on Windows — no behavior change on other platforms.
52+
"""
53+
return sys.platform == "win32" and len(scheme) == 1 and scheme.isalpha()
54+
55+
4456
AWS_PROFILE_NAME = "client.profile-name"
4557
AWS_REGION = "client.region"
4658
AWS_ACCESS_KEY_ID = "client.access-key-id"
@@ -336,13 +348,19 @@ def _import_file_io(io_impl: str, properties: Properties) -> FileIO | None:
336348

337349
def _infer_file_io_from_scheme(path: str, properties: Properties) -> FileIO | None:
338350
parsed_url = urlparse(path)
339-
if parsed_url.scheme:
340-
if file_ios := SCHEMA_TO_FILE_IO.get(parsed_url.scheme):
351+
scheme = parsed_url.scheme
352+
353+
if scheme and _is_windows_drive_letter(scheme):
354+
# Windows drive letter (e.g. 'C:\...') misidentified as a URL scheme
355+
scheme = "file"
356+
357+
if scheme:
358+
if file_ios := SCHEMA_TO_FILE_IO.get(scheme):
341359
for file_io_path in file_ios:
342360
if file_io := _import_file_io(file_io_path, properties):
343361
return file_io
344362
else:
345-
warnings.warn(f"No preferred file implementation for scheme: {parsed_url.scheme}", stacklevel=2)
363+
warnings.warn(f"No preferred file implementation for scheme: {scheme}", stacklevel=2)
346364
return None
347365

348366

‎pyiceberg/io/fsspec.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
InputStream,
9090
OutputFile,
9191
OutputStream,
92+
_is_windows_drive_letter,
9293
)
9394
from pyiceberg.typedef import Properties
9495
from pyiceberg.types import strtobool
@@ -480,6 +481,8 @@ def delete(self, location: str | InputFile | OutputFile) -> None:
480481

481482
def _get_fs_from_uri(self, uri: "ParseResult") -> AbstractFileSystem:
482483
"""Get a filesystem from a parsed URI, using hostname for ADLS account resolution."""
484+
if _is_windows_drive_letter(uri.scheme):
485+
return self.get_fs("file")
483486
if uri.scheme in _ADLS_SCHEMES:
484487
return self.get_fs(uri.scheme, uri.hostname)
485488
return self.get_fs(uri.scheme)

‎pyiceberg/io/pyarrow.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
InputStream,
122122
OutputFile,
123123
OutputStream,
124+
_is_windows_drive_letter,
124125
)
125126
from pyiceberg.io.fileformat import DataFileStatistics as DataFileStatistics
126127
from pyiceberg.io.fileformat import FileFormatFactory, FileFormatModel, FileFormatWriter
@@ -404,10 +405,12 @@ def parse_location(location: str, properties: Properties = EMPTY_DICT) -> tuple[
404405
"""Return (scheme, netloc, path) for the given location.
405406
406407
Uses DEFAULT_SCHEME and DEFAULT_NETLOC if scheme/netloc are missing.
408+
On Windows, single-letter schemes (drive letters like 'C') are treated
409+
as local file paths rather than URI schemes.
407410
"""
408411
uri = urlparse(location)
409412

410-
if not uri.scheme:
413+
if not uri.scheme or _is_windows_drive_letter(uri.scheme):
411414
default_scheme = properties.get("DEFAULT_SCHEME", "file")
412415
default_netloc = properties.get("DEFAULT_NETLOC", "")
413416
return default_scheme, default_netloc, os.path.abspath(location)

‎tests/io/test_io.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import os
1919
import pickle
20+
import sys
2021
import tempfile
2122
from typing import Any
2223

@@ -27,6 +28,7 @@
2728
PY_IO_IMPL,
2829
_import_file_io,
2930
_infer_file_io_from_scheme,
31+
_is_windows_drive_letter,
3032
load_file_io,
3133
)
3234
from pyiceberg.io.pyarrow import PyArrowFileIO
@@ -339,3 +341,31 @@ def test_infer_file_io_from_schema_unknown() -> None:
339341
_infer_file_io_from_scheme("unknown://bucket/path/", {})
340342

341343
assert str(w[0].message) == "No preferred file implementation for scheme: unknown"
344+
345+
346+
@pytest.mark.parametrize(
347+
"scheme",
348+
["s3", "hdfs", "file", "gs", ""],
349+
)
350+
def test_is_windows_drive_letter_false_for_real_schemes(scheme: str) -> None:
351+
assert _is_windows_drive_letter(scheme) is False
352+
353+
354+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-only behavior")
355+
@pytest.mark.parametrize("scheme", ["c", "D", "z"])
356+
def test_is_windows_drive_letter_true_on_windows(scheme: str) -> None:
357+
assert _is_windows_drive_letter(scheme) is True
358+
359+
360+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-only behavior")
361+
def test_infer_file_io_from_scheme_windows_drive_letter() -> None:
362+
# A Windows-style path like 'C:\Users\...' should be treated as a local file path,
363+
# not as a URL with scheme 'c'
364+
result = _infer_file_io_from_scheme(r"C:\Users\test\warehouse", {})
365+
assert isinstance(result, PyArrowFileIO)
366+
367+
368+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-only behavior")
369+
def test_infer_file_io_from_scheme_windows_drive_letter_lowercase() -> None:
370+
result = _infer_file_io_from_scheme(r"d:\data\iceberg\table", {})
371+
assert isinstance(result, PyArrowFileIO)

‎tests/io/test_pyarrow.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# pylint: disable=protected-access,unused-argument,redefined-outer-name
1818
import logging
1919
import os
20+
import sys
2021
import tempfile
2122
import uuid
2223
import warnings
@@ -2326,6 +2327,19 @@ def check_results(location: str, expected_schema: str, expected_netloc: str, exp
23262327
check_results("/root/tmp/foo.txt", "file", "", "/root/tmp/foo.txt")
23272328

23282329

2330+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-only behavior")
2331+
def test_parse_location_windows_drive_letter() -> None:
2332+
"""Windows drive letters like 'C:' should be treated as local file paths, not URL schemes."""
2333+
import os
2334+
2335+
for drive in ("C", "D", "c", "d"):
2336+
path = f"{drive}:\\Users\\test\\file.avro"
2337+
scheme, netloc, result_path = PyArrowFileIO.parse_location(path)
2338+
assert scheme == "file"
2339+
assert netloc == ""
2340+
assert result_path == os.path.abspath(path)
2341+
2342+
23292343
def test_make_compatible_name() -> None:
23302344
assert make_compatible_name("label/abc") == "label_x2Fabc"
23312345
assert make_compatible_name("label?abc") == "label_x3Fabc"

0 commit comments

Comments
 (0)