Skip to content

Commit 44bb523

Browse files
committed
autotest: Add a VSIFile helper class
1 parent 88c8300 commit 44bb523

3 files changed

Lines changed: 294 additions & 1 deletion

File tree

autotest/gcore/vsifile.py

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import pytest
3838
from lxml import etree
3939

40-
from osgeo import gdal
40+
from osgeo import gdal, ogr
4141

4242

4343
###############################################################################
@@ -1364,3 +1364,217 @@ def test_vsifile_CopyFileRestartable(tmp_vsimem):
13641364
assert retcode == 0
13651365
assert output_payload is None
13661366
assert gdal.VSIStatL(dstfilename).size == 3
1367+
1368+
1369+
###############################################################################
1370+
# Test VSIFile helper class
1371+
1372+
1373+
def test_vsifile_class_write_ascii(tmp_path):
1374+
1375+
fname = tmp_path / "test.txt"
1376+
1377+
lines = ["permission is hereby granted", "free of charge", "to any person"]
1378+
1379+
with gdaltest.vsi_open(fname, "w") as f:
1380+
assert f.tell() == 0
1381+
1382+
for line in lines:
1383+
f.write(line)
1384+
f.write("\n")
1385+
1386+
with open(fname, "r") as f:
1387+
assert [line.strip() for line in f.readlines()] == lines
1388+
1389+
1390+
def test_vsifile_class_read_ascii(tmp_path):
1391+
1392+
fname = str(tmp_path / "test.txt")
1393+
1394+
lines = ["permission is hereby granted", "free of charge", "to any person"]
1395+
1396+
with open(fname, "w", newline="\n") as f:
1397+
for line in lines:
1398+
f.write(line)
1399+
f.write("\n")
1400+
1401+
with pytest.raises(Exception):
1402+
f.write(b"some bytes")
1403+
1404+
# read entire file
1405+
with gdaltest.vsi_open(fname, "r") as f:
1406+
contents = f.read()
1407+
1408+
assert type(contents) is str
1409+
1410+
lines_in = [line.strip() for line in contents.strip().split("\n")]
1411+
assert lines_in == lines
1412+
1413+
# read some characters
1414+
f = gdaltest.vsi_open(fname)
1415+
assert f.read(10) == "permission"
1416+
1417+
# skip a character
1418+
f.seek(1, os.SEEK_CUR)
1419+
assert f.read(9) == "is hereby"
1420+
1421+
f.seek(0, os.SEEK_SET)
1422+
assert f.read(10) == "permission"
1423+
1424+
# jump to end
1425+
f.seek(0, os.SEEK_END)
1426+
assert f.read(10) == ""
1427+
1428+
f.seek(-7, os.SEEK_END)
1429+
assert f.read() == "person\n"
1430+
1431+
f.close()
1432+
f.close() # no harm in closing an already-closed file
1433+
1434+
1435+
def test_vsifile_class_read_binary(tmp_path):
1436+
1437+
fname = tmp_path / "test.wkb"
1438+
1439+
g = ogr.CreateGeometryFromWkt("POINT (15 17)")
1440+
wkb = g.ExportToWkb()
1441+
1442+
with open(fname, "wb") as f:
1443+
f.write(wkb)
1444+
1445+
# read entire file
1446+
with gdaltest.vsi_open(fname, "rb") as f:
1447+
contents = f.read()
1448+
1449+
assert type(contents) is bytes
1450+
1451+
assert contents == wkb
1452+
1453+
# read some bytes
1454+
f = gdaltest.vsi_open(fname, "rb")
1455+
assert f.read(5) == wkb[:5]
1456+
1457+
f.seek(10, os.SEEK_SET)
1458+
assert f.read(5) == wkb[10:15]
1459+
1460+
1461+
def test_vsifile_class_write_binary(tmp_path):
1462+
1463+
fname = tmp_path / "test.wkb"
1464+
1465+
g = ogr.CreateGeometryFromWkt("POINT (15 17)")
1466+
wkb = g.ExportToWkb()
1467+
1468+
with gdaltest.vsi_open(fname, "wb") as f:
1469+
f.write(wkb[:8])
1470+
f.write(wkb[8:])
1471+
1472+
with open(fname, "rb") as f:
1473+
assert f.read() == wkb
1474+
1475+
1476+
def random_lines():
1477+
import random
1478+
import string
1479+
1480+
lines = []
1481+
for i in range(50):
1482+
lines.append(
1483+
"".join([random.choice(string.ascii_letters) for j in range(20 + 3 * i)])
1484+
)
1485+
lines.append(" ")
1486+
lines.append("")
1487+
lines.append("theend")
1488+
lines.append("")
1489+
1490+
return lines
1491+
1492+
1493+
@pytest.mark.parametrize("terminating_newline", (True, False))
1494+
def test_vsifile_class_line_iteration(tmp_path, terminating_newline):
1495+
1496+
fname = str(tmp_path / "test.txt")
1497+
1498+
lines_out = random_lines()
1499+
1500+
with open(fname, "w") as f:
1501+
for line in lines_out:
1502+
f.write(line)
1503+
f.write("\n")
1504+
1505+
if not terminating_newline:
1506+
f.write("lastline")
1507+
lines_out.append("lastline")
1508+
1509+
with gdaltest.vsi_open(fname) as f:
1510+
lines_in = [line for line in f]
1511+
1512+
assert lines_in == lines_out
1513+
1514+
1515+
def test_vsifile_class_binary_line_iteration(tmp_path):
1516+
1517+
fname = str(tmp_path / "test.txt")
1518+
1519+
lines_out = [x.encode() for x in random_lines()]
1520+
1521+
with open(fname, "wb") as f:
1522+
for line in lines_out:
1523+
f.write(line)
1524+
f.write(b"\n")
1525+
1526+
with gdaltest.vsi_open(fname, "rb") as f:
1527+
lines_in = [line for line in f]
1528+
1529+
assert lines_in == lines_out
1530+
1531+
1532+
def test_vsifile_class_zipped_csv_reader(tmp_path):
1533+
1534+
test_csv = str(tmp_path / "input.csv")
1535+
test_zip = str(tmp_path / "input.zip")
1536+
1537+
import csv
1538+
import shutil
1539+
import zipfile
1540+
1541+
shutil.copy("../ogr/data/prime_meridian.csv", test_csv)
1542+
1543+
with zipfile.ZipFile(test_zip, "w") as zf:
1544+
zf.write(test_csv, arcname="input.csv")
1545+
1546+
with gdaltest.vsi_open(f"/vsizip/{test_zip}/input.csv") as f:
1547+
records = [x for x in csv.DictReader(f)]
1548+
1549+
assert len(records) == 4
1550+
assert (
1551+
records[2]["INFORMATION_SOURCE"]
1552+
== "Institut Geographique National (IGN), Paris"
1553+
)
1554+
1555+
1556+
def test_vsifile_class_file_does_not_exist(tmp_path):
1557+
1558+
with pytest.raises(OSError, match="No such file or directory"):
1559+
gdaltest.vsi_open(tmp_path / "does_not_exist.txt")
1560+
1561+
1562+
def test_vsifile_class_read_from_closed_file(tmp_path):
1563+
1564+
with gdaltest.vsi_open(tmp_path / "out.txt", "w") as f:
1565+
f.write("abc")
1566+
1567+
with pytest.raises(ValueError, match="closed file"):
1568+
f.seek(0)
1569+
1570+
1571+
def test_vsifile_class_append(tmp_vsimem):
1572+
1573+
fname = tmp_vsimem / "out.txt"
1574+
1575+
with gdaltest.vsi_open(fname, "w") as f:
1576+
f.write("abc")
1577+
with gdaltest.vsi_open(fname, "a") as f:
1578+
f.write("def")
1579+
with gdaltest.vsi_open(fname) as f:
1580+
assert f.read() == "abcdef"

autotest/pymod/gdaltest.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,3 +2116,80 @@ def reopen(ds, update=False, open_options=None):
21162116
allowed_drivers=[ds_drv.GetDescription()],
21172117
open_options=open_options,
21182118
)
2119+
2120+
2121+
# VSIFile helper class
2122+
2123+
2124+
class VSIFile:
2125+
def __init__(self, path, mode, encoding="utf-8"):
2126+
self._path = path
2127+
self._mode = mode
2128+
2129+
self._binary = "b" in mode
2130+
self._encoding = encoding
2131+
2132+
self._fp = gdal.VSIFOpenExL(self._path, self._mode, True)
2133+
if self._fp is None:
2134+
raise OSError(gdal.VSIGetLastErrorMsg())
2135+
2136+
self._closed = False
2137+
self._buffer = None
2138+
2139+
def __enter__(self):
2140+
return self
2141+
2142+
def __exit__(self, *args):
2143+
self.close()
2144+
2145+
def __iter__(self):
2146+
return self
2147+
2148+
def __next__(self):
2149+
line = gdal.CPLReadLineL(self._fp)
2150+
if line is None:
2151+
raise StopIteration
2152+
if self._binary:
2153+
return line.encode()
2154+
return line
2155+
2156+
def close(self):
2157+
if self._closed:
2158+
return
2159+
2160+
self._closed = True
2161+
gdal.VSIFCloseL(self._fp)
2162+
2163+
def read(self, size=-1):
2164+
if size == -1:
2165+
pos = self.tell()
2166+
self.seek(0, 2)
2167+
size = self.tell()
2168+
self.seek(pos)
2169+
2170+
raw = gdal.VSIFReadL(1, size, self._fp)
2171+
2172+
if self._binary:
2173+
return bytes(raw)
2174+
else:
2175+
return raw.decode(self._encoding)
2176+
2177+
def write(self, x):
2178+
2179+
if self._binary:
2180+
assert type(x) in (bytes, bytearray, memoryview)
2181+
else:
2182+
assert type(x) is str
2183+
x = x.encode(self._encoding)
2184+
2185+
gdal.VSIFWriteL(x, 1, len(x), self._fp)
2186+
2187+
def seek(self, offset, whence=0):
2188+
gdal.VSIFSeekL(self._fp, offset, whence)
2189+
2190+
def tell(self):
2191+
return gdal.VSIFTellL(self._fp)
2192+
2193+
2194+
def vsi_open(path, mode="r"):
2195+
return VSIFile(path, mode)

swig/include/cpl.i

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,8 @@ int VSIFWriteL( const char *, int, int, VSILFILE *fp );
943943

944944
/* VSIFReadL() handled specially in python/gdal_python.i */
945945

946+
const char* CPLReadLineL(VSILFILE* fp);
947+
946948
void VSICurlClearCache();
947949
void VSICurlPartialClearCache( const char* utf8_path );
948950

0 commit comments

Comments
 (0)