[Git][debian-gis-team/fiona][upstream] New upstream version 1.8.18
Bas Couwenberg
gitlab at salsa.debian.org
Wed Nov 18 03:57:18 GMT 2020
Bas Couwenberg pushed to branch upstream at Debian GIS Project / fiona
Commits:
1c7d94a6 by Bas Couwenberg at 2020-11-18T04:21:24+01:00
New upstream version 1.8.18
- - - - -
27 changed files:
- + .github/workflows/rstcheck.yml
- .travis.yml
- CHANGES.txt
- fiona/__init__.py
- fiona/_env.pyx
- fiona/_transform.pyx
- fiona/collection.py
- fiona/drvsupport.py
- fiona/errors.py
- fiona/fio/info.py
- fiona/fio/load.py
- fiona/ogrext.pyx
- + pytest.ini
- requirements-ci.txt
- requirements.txt
- − setup.cfg
- setup.py
- tests/conftest.py
- tests/test_bounds.py
- tests/test_collection.py
- + tests/test_cursor_interruptions.py
- tests/test_datetime.py
- + tests/test_driver_options.py
- tests/test_drvsupport.py
- tests/test_fio_load.py
- tests/test_slice.py
- tests/test_transform.py
Changes:
=====================================
.github/workflows/rstcheck.yml
=====================================
@@ -0,0 +1,26 @@
+name: rstcheck
+
+# Run this workflow every time a new commit pushed to your repository
+on: [push, pull_request]
+
+jobs:
+ rstcheck:
+ name: rstcheck
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout at v2
+
+ - name: Set up Python
+ uses: actions/setup-python at v2
+ with:
+ python-version: 3.8
+
+ - name: Install Python dependencies
+ run: |
+ python -m pip install sphinx==3.2.1 rstcheck==3.3.1
+
+ - name: Run rstcheck
+ run: |
+ rstcheck -r --ignore-directives automodule --ignore-substitutions version,release,today .
=====================================
.travis.yml
=====================================
@@ -56,19 +56,19 @@ matrix:
# Test all supported python versions with latest stable gdal release
- python: "2.7"
env:
- GDALVERSION="3.1.2"
+ GDALVERSION="3.1.3"
PROJVERSION="6.3.2"
- python: "3.6"
env:
- GDALVERSION="3.1.2"
+ GDALVERSION="3.1.3"
PROJVERSION="6.3.2"
- python: "3.7"
env:
- GDALVERSION="3.1.2"
+ GDALVERSION="3.1.3"
PROJVERSION="6.3.2"
- python: "3.8"
env:
- GDALVERSION="3.1.2"
+ GDALVERSION="3.1.3"
PROJVERSION="6.3.2"
# Test master
@@ -92,10 +92,8 @@ addons:
- sqlite3
before_install:
- - pip install -U pip
- - pip install setuptools==36.0.1
- - pip install wheel
- - "python -m pip install -r requirements-ci.txt"
+ - python -m pip install -U pip
+ - python -m pip install -r requirements-ci.txt
- python -m pip wheel -r requirements-dev.txt
- python -m pip install -r requirements-dev.txt
- export PATH=$GDALINST/gdal-$GDALVERSION/bin:$GDALINST/proj-$PROJVERSION/bin:$PATH
@@ -119,9 +117,6 @@ install:
script:
- python -m pytest -m "not wheel" --cov fiona --cov-report term-missing
- # Check documentation
- - rstcheck -r --ignore-directives automodule --ignore-substitutions version,release,today .
-
after_script:
- python setup.py clean
=====================================
CHANGES.txt
=====================================
@@ -3,6 +3,17 @@ Changes
All issue numbers are relative to https://github.com/Toblerity/Fiona/issues.
+1.8.18 (2020-11-17)
+-------------------
+
+- The precision option of transform has been fixed for the case of
+ GeometryCollections (#971, #972).
+- Added missing --co (creation) option to fio-load (#390).
+- If the certifi package can be imported, its certificate store location will
+ be passed to GDAL during import of fiona._env unless CURL_CA_BUNDLE is
+ already set.
+- Warn when feature fields named "" are found (#955).
+
1.8.17 (2020-09-09)
-------------------
=====================================
fiona/__init__.py
=====================================
@@ -105,7 +105,7 @@ with fiona._loading.add_gdal_dll_directories():
__all__ = ['bounds', 'listlayers', 'open', 'prop_type', 'prop_width']
-__version__ = "1.8.17"
+__version__ = "1.8.18"
__gdal_version__ = get_gdal_release_name()
gdal_version = get_gdal_version_tuple()
=====================================
fiona/_env.pyx
=====================================
@@ -55,6 +55,11 @@ code_map = {
log = logging.getLogger(__name__)
+try:
+ import certifi
+ os.environ.setdefault("CURL_CA_BUNDLE", certifi.where())
+except ImportError:
+ pass
cdef bint is_64bit = sys.maxsize > 2 ** 32
=====================================
fiona/_transform.pyx
=====================================
@@ -162,8 +162,8 @@ def _transform_geom(
g = geom
if precision >= 0:
-
- if g['type'] == 'Point':
+
+ def round_point(g):
coords = list(g['coordinates'])
x, y = coords[:2]
x = round(x, precision)
@@ -172,8 +172,10 @@ def _transform_geom(
if len(coords) == 3:
z = coords[2]
new_coords.append(round(z, precision))
-
- elif g['type'] in ['LineString', 'MultiPoint']:
+ return new_coords
+
+
+ def round_linestring(g):
coords = list(zip(*g['coordinates']))
xp, yp = coords[:2]
xp = [round(v, precision) for v in xp]
@@ -184,8 +186,10 @@ def _transform_geom(
new_coords = list(zip(xp, yp, zp))
else:
new_coords = list(zip(xp, yp))
+ return new_coords
+
- elif g['type'] in ['Polygon', 'MultiLineString']:
+ def round_polygon(g):
new_coords = []
for piece in g['coordinates']:
coords = list(zip(*piece))
@@ -198,8 +202,9 @@ def _transform_geom(
new_coords.append(list(zip(xp, yp, zp)))
else:
new_coords.append(list(zip(xp, yp)))
+ return new_coords
- elif g['type'] == 'MultiPolygon':
+ def round_multipolygon(g):
parts = g['coordinates']
new_coords = []
for part in parts:
@@ -216,7 +221,24 @@ def _transform_geom(
else:
inner_coords.append(list(zip(xp, yp)))
new_coords.append(inner_coords)
-
- g['coordinates'] = new_coords
+ return new_coords
+
+ def round_geometry(g):
+ if g['type'] == 'Point':
+ g['coordinates'] = round_point(g)
+ elif g['type'] in ['LineString', 'MultiPoint']:
+ g['coordinates'] = round_linestring(g)
+ elif g['type'] in ['Polygon', 'MultiLineString']:
+ g['coordinates'] = round_polygon(g)
+ elif g['type'] == 'MultiPolygon':
+ g['coordinates'] = round_multipolygon(g)
+ else:
+ raise RuntimeError("Unsupported geometry type: {}".format(g['type']))
+
+ if g['type'] == 'GeometryCollection':
+ for _g in g['geometries']:
+ round_geometry(_g)
+ else:
+ round_geometry(g)
return g
=====================================
fiona/collection.py
=====================================
@@ -78,19 +78,6 @@ class Collection(object):
if archive and not isinstance(archive, string_types):
raise TypeError("invalid archive: %r" % archive)
-
- # Check GDAL version against drivers
-
- if driver in driver_mode_mingdal[mode] and get_gdal_version_tuple() < driver_mode_mingdal[mode][driver]:
- min_gdal_version = ".".join(list(map(str, driver_mode_mingdal[mode][driver])))
-
- raise DriverError(
- "{driver} driver requires at least GDAL {min_gdal_version} for mode '{mode}', "
- "Fiona was compiled against: {gdal}".format(driver=driver,
- mode=mode,
- min_gdal_version=min_gdal_version,
- gdal=get_gdal_release_name()))
-
self.session = None
self.iterator = None
self._len = 0
@@ -104,6 +91,17 @@ class Collection(object):
self.ignore_fields = ignore_fields
self.ignore_geometry = bool(ignore_geometry)
+ # Check GDAL version against drivers
+ if driver in driver_mode_mingdal[mode] and get_gdal_version_tuple() < driver_mode_mingdal[mode][driver]:
+ min_gdal_version = ".".join(list(map(str, driver_mode_mingdal[mode][driver])))
+
+ raise DriverError(
+ "{driver} driver requires at least GDAL {min_gdal_version} for mode '{mode}', "
+ "Fiona was compiled against: {gdal}".format(driver=driver,
+ mode=mode,
+ min_gdal_version=min_gdal_version,
+ gdal=get_gdal_release_name()))
+
if vsi:
self.path = vfs.vsi_path(path, vsi, archive)
path = parse_path(self.path)
@@ -362,7 +360,7 @@ class Collection(object):
raise IOError("collection not open for writing")
self.session.writerecs(records, self)
self._len = self.session.get_length()
- self._bounds = self.session.get_extent()
+ self._bounds = None
def write(self, record):
"""Stages a record for writing to disk."""
@@ -448,7 +446,7 @@ class Collection(object):
self.session.sync(self)
new_len = self.session.get_length()
self._len = new_len > self._len and new_len or self._len
- self._bounds = self.session.get_extent()
+ self._bounds = None
def close(self):
"""In append or write mode, flushes data to disk, then ends
=====================================
fiona/drvsupport.py
=====================================
@@ -41,7 +41,6 @@ supported_drivers = dict([
# multi-layer
("FileGDB", "raw"),
("OpenFileGDB", "r"),
- ("FlatGeobuf", "r"),
# ESRI Personal GeoDatabase PGeo No Yes No, needs ODBC library
# ESRI ArcSDE SDE No Yes No, needs ESRI SDE
# ESRIJSON ESRIJSON No Yes Yes
@@ -49,6 +48,7 @@ supported_drivers = dict([
# ESRI Shapefile ESRI Shapefile Yes Yes Yes
("ESRI Shapefile", "raw"),
# FMEObjects Gateway FMEObjects Gateway No Yes No, needs FME
+ ("FlatGeobuf", "rw"),
# GeoJSON GeoJSON Yes Yes Yes
("GeoJSON", "raw"),
# GeoJSONSeq GeoJSON sequences Yes Yes Yes
@@ -150,11 +150,13 @@ supported_drivers = dict([
driver_mode_mingdal = {
'r': {'GPKG': (1, 11, 0),
- 'GeoJSONSeq': (2, 4, 0)},
+ 'GeoJSONSeq': (2, 4, 0),
+ 'FlatGeobuf': (3, 1, 0)},
'w': {'GPKG': (1, 11, 0),
'PCIDSK': (2, 0, 0),
- 'GeoJSONSeq': (2, 4, 0)},
+ 'GeoJSONSeq': (2, 4, 0),
+ 'FlatGeobuf': (3, 1, 3)},
'a': {'GPKG': (1, 11, 0),
'GeoJSON': (2, 1, 0),
@@ -249,7 +251,8 @@ _driver_field_type_unsupported = {
'BNA': None,
'DXF': None,
'PCIDSK': (2, 1, 0),
- 'FileGDB': None
+ 'FileGDB': None,
+ 'FlatGeobuf': None
},
'datetime': {
'ESRI Shapefile': None,
@@ -266,7 +269,8 @@ _driver_field_type_unsupported = {
'BNA': None,
'DXF': None,
'PCIDSK': (2, 1, 0),
- 'FileGDB': None
+ 'FileGDB': None,
+ 'FlatGeobuf': None
}
}
=====================================
fiona/errors.py
=====================================
@@ -65,3 +65,7 @@ class GDALVersionError(FionaError):
class FionaDeprecationWarning(UserWarning):
"""A warning about deprecation of Fiona features"""
+
+
+class FeatureWarning(UserWarning):
+ """A warning about serialization of a feature"""
=====================================
fiona/fio/info.py
=====================================
@@ -9,6 +9,7 @@ from cligj import indent_opt
import fiona
import fiona.crs
+from fiona.errors import DriverError
from fiona.fio import options, with_context_env
@@ -47,7 +48,13 @@ def info(ctx, input, indent, meta_member, layer):
try:
with fiona.open(input, layer=layer) as src:
info = src.meta
- info.update(bounds=src.bounds, name=src.name)
+ info.update(name=src.name)
+ try:
+ info.update(bounds=src.bounds)
+ except DriverError:
+ info.update(bounds=None)
+ logger.debug("Setting 'bounds' to None - driver "
+ "was not able to calculate bounds")
try:
info.update(count=len(src))
except TypeError:
=====================================
fiona/fio/load.py
=====================================
@@ -13,6 +13,37 @@ from fiona.schema import FIELD_TYPES_MAP_REV
from fiona.transform import transform_geom
+def _cb_key_val(ctx, param, value):
+ """
+ click callback to validate `--opt KEY1=VAL1 --opt KEY2=VAL2` and collect
+ in a dictionary like the one below, which is what the CLI function receives.
+ If no value or `None` is received then an empty dictionary is returned.
+
+ {
+ 'KEY1': 'VAL1',
+ 'KEY2': 'VAL2'
+ }
+
+ Note: `==VAL` breaks this as `str.split('=', 1)` is used.
+
+ """
+ if not value:
+ return {}
+ else:
+ out = {}
+ for pair in value:
+ if "=" not in pair:
+ raise click.BadParameter(
+ "Invalid syntax for KEY=VAL arg: {}".format(pair)
+ )
+ else:
+ k, v = pair.split("=", 1)
+ k = k.lower()
+ v = v.lower()
+ out[k] = None if v.lower() in ["none", "null", "nil", "nada"] else v
+ return out
+
+
@click.command(short_help="Load GeoJSON to a dataset in another format.")
@click.argument('output', required=True)
@click.option('-f', '--format', '--driver', 'driver', required=True,
@@ -21,16 +52,30 @@ from fiona.transform import transform_geom
@click.option('--dst-crs', '--dst_crs',
help="Destination CRS. Defaults to --src-crs when not given.")
@cligj.features_in_arg
- at click.option('--layer', metavar="INDEX|NAME", callback=options.cb_layer,
- help="Load features into specified layer. Layers use "
- "zero-based numbering when accessed by index.")
+ at click.option(
+ "--layer",
+ metavar="INDEX|NAME",
+ callback=options.cb_layer,
+ help="Load features into specified layer. Layers use "
+ "zero-based numbering when accessed by index.",
+)
+ at click.option(
+ "--co",
+ "--profile",
+ "creation_options",
+ metavar="NAME=VALUE",
+ multiple=True,
+ callback=_cb_key_val,
+ help="Driver specific creation options. See the documentation for the selected output driver for more information.",
+)
@click.pass_context
@with_context_env
-def load(ctx, output, driver, src_crs, dst_crs, features, layer):
+def load(ctx, output, driver, src_crs, dst_crs, features, layer, creation_options):
"""Load features from JSON to a file in another format.
The input is a GeoJSON feature collection or optionally a sequence of
GeoJSON feature objects.
+
"""
logger = logging.getLogger(__name__)
@@ -60,11 +105,14 @@ def load(ctx, output, driver, src_crs, dst_crs, features, layer):
for k, v in first['properties'].items()])
with fiona.open(
- output, 'w',
- driver=driver,
- crs=dst_crs,
- schema=schema,
- layer=layer) as dst:
+ output,
+ "w",
+ driver=driver,
+ crs=dst_crs,
+ schema=schema,
+ layer=layer,
+ **creation_options
+ ) as dst:
dst.write(first)
dst.writerecords(source)
=====================================
fiona/ogrext.pyx
=====================================
@@ -30,7 +30,7 @@ from fiona.env import Env
from fiona.errors import (
DriverError, DriverIOError, SchemaError, CRSError, FionaValueError,
TransactionError, GeometryTypeValidationError, DatasetDeleteError,
- FionaDeprecationWarning)
+ FeatureWarning, FionaDeprecationWarning)
from fiona.compat import OrderedDict
from fiona.rfc3339 import parse_date, parse_datetime, parse_time
from fiona.rfc3339 import FionaDateType, FionaDateTimeType, FionaTimeType
@@ -192,12 +192,14 @@ cdef class FeatureBuilder:
for i in range(OGR_F_GetFieldCount(feature)):
fdefn = OGR_F_GetFieldDefnRef(feature, i)
if fdefn == NULL:
- raise ValueError("Null feature definition")
+ raise ValueError("NULL field definition at index {}".format(i))
key_c = OGR_Fld_GetNameRef(fdefn)
if key_c == NULL:
- raise ValueError("Null field name reference")
+ raise ValueError("NULL field name reference at index {}".format(i))
key_b = key_c
key = key_b.decode(encoding)
+ if not key:
+ warnings.warn("Empty field name at index {}".format(i))
if key in ignore_fields:
continue
@@ -498,12 +500,14 @@ cdef class Session:
cdef object _fileencoding
cdef object _encoding
cdef object collection
+ cdef bint cursor_interrupted
def __init__(self):
self.cogr_ds = NULL
self.cogr_layer = NULL
self._fileencoding = None
self._encoding = None
+ self.cursor_interrupted = False
def __dealloc__(self):
self.stop()
@@ -625,7 +629,7 @@ cdef class Session:
def get_length(self):
if self.cogr_layer == NULL:
raise ValueError("Null layer")
- return OGR_L_GetFeatureCount(self.cogr_layer, 0)
+ return self._get_feature_count(0)
def get_driver(self):
cdef void *cogr_driver = GDALGetDatasetDriver(self.cogr_ds)
@@ -662,15 +666,15 @@ cdef class Session:
for i from 0 <= i < n:
cogr_fielddefn = OGR_FD_GetFieldDefn(cogr_featuredefn, i)
if cogr_fielddefn == NULL:
- raise ValueError("Null field definition")
+ raise ValueError("NULL field definition at index {}".format(i))
key_c = OGR_Fld_GetNameRef(cogr_fielddefn)
+ if key_c == NULL:
+ raise ValueError("NULL field name reference at index {}".format(i))
key_b = key_c
-
- if not bool(key_b):
- raise ValueError("Invalid field name ref: %s" % key)
-
key = key_b.decode(encoding)
+ if not key:
+ warnings.warn("Empty field name at index {}".format(i), FeatureWarning)
if key in ignore_fields:
log.debug("By request, ignoring field %r", key)
@@ -848,7 +852,18 @@ cdef class Session:
raise ValueError("Null layer")
result = OGR_L_GetExtent(self.cogr_layer, &extent, 1)
+ self.cursor_interrupted = True
+ if result != OGRERR_NONE:
+ raise DriverError("Driver was not able to calculate bounds")
return (extent.MinX, extent.MinY, extent.MaxX, extent.MaxY)
+
+
+ cdef int _get_feature_count(self, force=0):
+ if self.cogr_layer == NULL:
+ raise ValueError("Null layer")
+ self.cursor_interrupted = True
+ return OGR_L_GetFeatureCount(self.cogr_layer, force)
+
def has_feature(self, fid):
"""Provides access to feature data by FID.
@@ -900,7 +915,7 @@ cdef class Session:
index = item
# from the back
if index < 0:
- ftcount = OGR_L_GetFeatureCount(self.cogr_layer, 0)
+ ftcount = self._get_feature_count(0)
if ftcount == -1:
raise IndexError(
"collection's dataset does not support negative indexes")
@@ -992,7 +1007,8 @@ cdef class WritingSession(Session):
if not CPLCheckForFile(path_c, NULL):
log.debug("File doesn't exist. Creating a new one...")
- cogr_ds = gdal_create(cogr_driver, path_c, {})
+ with Env(GDAL_VALIDATE_CREATION_OPTIONS="NO"):
+ cogr_ds = gdal_create(cogr_driver, path_c, kwargs)
else:
if collection.driver == "GeoJSON":
@@ -1374,9 +1390,9 @@ cdef class Iterator:
warnings.warn("Layer does not support" \
" OLC_FASTFEATURECOUNT, negative slices or start values other than zero" \
" may be slow.", RuntimeWarning)
- self.ftcount = OGR_L_GetFeatureCount(session.cogr_layer, 1)
+ self.ftcount = session._get_feature_count(1)
else:
- self.ftcount = OGR_L_GetFeatureCount(session.cogr_layer, 0)
+ self.ftcount = session._get_feature_count(0)
if self.ftcount == -1 and ((start is not None and start < 0) or
(stop is not None and stop < 0)):
@@ -1450,20 +1466,27 @@ cdef class Iterator:
raise StopIteration
# Set read cursor to next_item position
- if self.step > 1 and self.fastindex:
- exc_wrap_int(OGR_L_SetNextByIndex(session.cogr_layer, self.next_index))
- elif self.step > 1 and not self.fastindex and not self.next_index == self.start:
- # GDALs default implementation of SetNextByIndex is calling ResetReading() and then
- # calling GetNextFeature n times. We can shortcut that if we know the previous index.
- # OGR_L_GetNextFeature increments cursor by 1, therefore self.step - 1 as one increment was performed when feature is read
- for _ in range(self.step - 1):
- cogr_feature = OGR_L_GetNextFeature(session.cogr_layer)
- if cogr_feature == NULL:
- raise StopIteration
- elif self.step > 1 and not self.fastindex and self.next_index == self.start:
- exc_wrap_int(OGR_L_SetNextByIndex(session.cogr_layer, self.next_index))
- elif self.step < 0:
+ if session.cursor_interrupted:
+ if not self.fastindex:
+ warnings.warn("Sequential read of iterator was interrupted. Resetting iterator. "
+ "This can negatively impact the performance.", RuntimeWarning)
exc_wrap_int(OGR_L_SetNextByIndex(session.cogr_layer, self.next_index))
+ session.cursor_interrupted = False
+ else:
+ if self.step > 1 and self.fastindex:
+ exc_wrap_int(OGR_L_SetNextByIndex(session.cogr_layer, self.next_index))
+ elif self.step > 1 and not self.fastindex and not self.next_index == self.start:
+ # GDALs default implementation of SetNextByIndex is calling ResetReading() and then
+ # calling GetNextFeature n times. We can shortcut that if we know the previous index.
+ # OGR_L_GetNextFeature increments cursor by 1, therefore self.step - 1 as one increment was performed when feature is read
+ for _ in range(self.step - 1):
+ cogr_feature = OGR_L_GetNextFeature(session.cogr_layer)
+ if cogr_feature == NULL:
+ raise StopIteration
+ elif self.step > 1 and not self.fastindex and self.next_index == self.start:
+ exc_wrap_int(OGR_L_SetNextByIndex(session.cogr_layer, self.next_index))
+ elif self.step < 0:
+ exc_wrap_int(OGR_L_SetNextByIndex(session.cogr_layer, self.next_index))
# set the next index
self.next_index += self.step
=====================================
pytest.ini
=====================================
@@ -0,0 +1,13 @@
+[pytest]
+filterwarnings =
+ ignore:.*Sequential read of iterator was interrupted*:RuntimeWarning
+ ignore:.*negative slices or start values other than zero may be slow*:RuntimeWarning
+ ignore:.*negative step size may be slow*:RuntimeWarning
+ ignore:.*is buggy and will be removed in Fiona 2.0.*
+
+markers =
+ iconv: marks tests that require gdal to be compiled with iconv
+ network: marks tests that require a network connection
+ wheel: marks test that only works when installed from wheel
+
+testpaths = tests
=====================================
requirements-ci.txt
=====================================
@@ -1,6 +1 @@
coveralls
-# sphinx 3.x does not support python 2.7
-sphinx==3.0.2 ; python_version > '3.0'
-sphinx==1.8.5 ; python_version < '3.0'
-rstcheck==3.3.1
-
=====================================
requirements.txt
=====================================
@@ -4,3 +4,4 @@ cligj==0.5.0
munch==2.3.2
six==1.11.0
enum34==1.1.6 ; python_version < '3.4'
+certifi
=====================================
setup.cfg deleted
=====================================
@@ -1,7 +0,0 @@
-[tool:pytest]
-testpaths = tests
-markers =
- slow
- network
- iconv
- wheel
=====================================
setup.py
=====================================
@@ -284,6 +284,7 @@ elif "clean" not in sys.argv:
requirements = [
'attrs>=17',
+ 'certifi',
'click>=4.0,<8',
'cligj>=0.5',
'click-plugins>=1.0',
=====================================
tests/conftest.py
=====================================
@@ -28,7 +28,8 @@ driver_extensions = {'DXF': 'dxf',
'GeoJSONSeq': 'geojsons',
'GMT': 'gmt',
'OGR_GMT': 'gmt',
- 'BNA': 'bna'}
+ 'BNA': 'bna',
+ 'FlatGeobuf': 'fgb'}
def pytest_report_header(config):
@@ -395,6 +396,12 @@ def testdata_generator():
}
return special_records2.get(driver, get_records(driver, range))
+ def get_create_kwargs(driver):
+ kwargs = {
+ 'FlatGeobuf': {'SPATIAL_INDEX': False}
+ }
+ return kwargs.get(driver, {})
+
def test_equal(driver, val_in, val_out):
is_good = True
is_good = is_good and val_in['geometry'] == val_out['geometry']
@@ -437,7 +444,7 @@ def testdata_generator():
the properties of the generated records can be found in a record
"""
return get_schema(driver), get_crs(driver), get_records(driver, range1), get_records2(driver, range2),\
- test_equal
+ test_equal, get_create_kwargs(driver)
return _testdata_generator
=====================================
tests/test_bounds.py
=====================================
@@ -1,4 +1,9 @@
+import pytest
import fiona
+from fiona.drvsupport import supported_drivers, _driver_supports_mode
+from fiona.errors import DriverError
+from .conftest import driver_extensions
+from fiona.env import GDALVersion
def test_bounds_point():
@@ -17,5 +22,48 @@ def test_bounds_polygon():
def test_bounds_z():
- g = {'type': 'Point', 'coordinates': [10,10,10]}
+ g = {'type': 'Point', 'coordinates': [10, 10, 10]}
assert fiona.bounds(g) == (10, 10, 10, 10)
+
+
+ignore_write_drivers = set(['CSV', 'GPX', 'GPSTrackMaker', 'DXF', 'DGN', 'MapInfo File'])
+write_drivers = [driver for driver, raw in supported_drivers.items() if
+ _driver_supports_mode(driver, 'w') and driver not in ignore_write_drivers]
+
+
+ at pytest.mark.parametrize('driver', write_drivers)
+def test_bounds(tmpdir, driver):
+ """Test if bounds are correctly calculated after writing
+
+ """
+
+ if driver == 'BNA' and GDALVersion.runtime() < GDALVersion(2, 0):
+ # BNA driver segfaults with gdal 1.11
+ return
+
+ extension = driver_extensions.get(driver, "bar")
+ path = str(tmpdir.join('foo.{}'.format(extension)))
+
+ with fiona.open(path, 'w',
+ driver=driver,
+ schema={'geometry': 'Point',
+ 'properties': [('title', 'str')]},
+ fiona_force_driver=True) as c:
+
+ c.writerecords([{'geometry': {'type': 'Point', 'coordinates': (1.0, 10.0)},
+ 'properties': {'title': 'One'}}])
+
+ try:
+ bounds = c.bounds
+ assert bounds == (1.0, 10.0, 1.0, 10.0)
+ except Exception as e:
+ assert isinstance(e, DriverError)
+
+ c.writerecords([{'geometry': {'type': 'Point', 'coordinates': (2.0, 20.0)},
+ 'properties': {'title': 'Two'}}])
+
+ try:
+ bounds = c.bounds
+ assert bounds == (1.0, 10.0, 2.0, 20.0)
+ except Exception as e:
+ assert isinstance(e, DriverError)
=====================================
tests/test_collection.py
=====================================
@@ -939,3 +939,26 @@ def test_mask_polygon_triangle(tmpdir, driver, filename):
items = list(
c.items(mask={'type': 'Polygon', 'coordinates': (((2.0, 2.0), (4.0, 4.0), (4.0, 6.0), (2.0, 2.0)),)}))
assert len(items) == 15
+
+
+def test_collection__empty_column_name(tmpdir):
+ """Based on pull #955"""
+ tmpfile = str(tmpdir.join("test_empty.geojson"))
+ with pytest.warns(UserWarning, match="Empty field name at index 0"):
+ with fiona.open(tmpfile, "w", driver="GeoJSON", schema={
+ "geometry": "Point",
+ "properties": {"": "str", "name": "str"}
+ }) as tmp:
+ tmp.writerecords([{
+ "geometry": {"type": "Point", "coordinates": [ 8, 49 ] },
+ "properties": { "": "", "name": "test" }
+ }])
+
+ with fiona.open(tmpfile) as tmp:
+ with pytest.warns(UserWarning, match="Empty field name at index 0"):
+ assert tmp.schema == {
+ "geometry": "Point",
+ "properties": {"": "str", "name": "str"}
+ }
+ with pytest.warns(UserWarning, match="Empty field name at index 0"):
+ next(tmp)
=====================================
tests/test_cursor_interruptions.py
=====================================
@@ -0,0 +1,158 @@
+import fiona
+import pytest
+from fiona.drvsupport import driver_mode_mingdal, _driver_supports_mode
+from fiona.errors import DriverError
+from tests.conftest import get_temp_filename
+
+
+ at pytest.mark.parametrize('driver', [driver for driver in driver_mode_mingdal['w'].keys()
+ if _driver_supports_mode(driver, 'w')])
+def test_write_getextent(tmpdir, driver, testdata_generator):
+ """
+ Test if a call to OGR_L_GetExtent has side effects for writing
+
+ """
+
+ schema, crs, records1, records2, test_equal, create_kwargs = testdata_generator(driver, range(0, 10), range(10, 20))
+ path = str(tmpdir.join(get_temp_filename(driver)))
+ positions = set([int(r['properties']['position']) for r in records1 + records2])
+
+ with fiona.open(path, 'w',
+ driver=driver,
+ crs=crs,
+ schema=schema,
+ **create_kwargs) as c:
+ c.writerecords(records1)
+
+ # Call to OGR_L_GetExtent
+ try:
+ c.bounds
+ except DriverError:
+ pass
+
+ c.writerecords(records2)
+
+ with fiona.open(path) as c:
+ data = set([int(f['properties']['position']) for f in c])
+ assert len(positions) == len(data)
+ for p in positions:
+ assert p in data
+
+
+ at pytest.mark.parametrize('driver', [driver for driver in driver_mode_mingdal['w'].keys()
+ if _driver_supports_mode(driver, 'w')])
+def test_read_getextent(tmpdir, driver, testdata_generator):
+ """
+ Test if a call to OGR_L_GetExtent has side effects for reading
+
+ """
+
+ schema, crs, records1, records2, test_equal, create_kwargs = testdata_generator(driver, range(0, 10), range(10, 20))
+ path = str(tmpdir.join(get_temp_filename(driver)))
+ positions = set([int(r['properties']['position']) for r in records1 + records2])
+
+ with fiona.open(path, 'w',
+ driver=driver,
+ crs=crs,
+ schema=schema,
+ **create_kwargs) as c:
+ c.writerecords(records1)
+ c.writerecords(records2)
+
+ with fiona.open(path) as c:
+ data = set()
+ for _ in range(len(records1)):
+ f = next(c)
+ data.add(int(f['properties']['position']))
+
+ # Call to OGR_L_GetExtent
+ try:
+ c.bounds
+ except DriverError:
+ pass
+
+ for _ in range(len(records1)):
+ f = next(c)
+ data.add(int(f['properties']['position']))
+ assert len(positions) == len(data)
+ for p in positions:
+ assert p in data
+
+
+ at pytest.mark.parametrize('driver', [driver for driver in driver_mode_mingdal['w'].keys()
+ if _driver_supports_mode(driver, 'w')])
+def test_write_getfeaturecount(tmpdir, driver, testdata_generator):
+ """
+ Test if a call to OGR_L_GetFeatureCount has side effects for writing
+
+ """
+
+ schema, crs, records1, records2, test_equal, create_kwargs = testdata_generator(driver, range(0, 10), range(10, 20))
+ path = str(tmpdir.join(get_temp_filename(driver)))
+ positions = set([int(r['properties']['position']) for r in records1 + records2])
+
+ with fiona.open(path, 'w',
+ driver=driver,
+ crs=crs,
+ schema=schema,
+ **create_kwargs) as c:
+ c.writerecords(records1)
+
+ # Call to OGR_L_GetFeatureCount
+ try:
+ assert len(c) == len(records1)
+ except TypeError:
+ pass
+ c.writerecords(records2)
+
+ with fiona.open(path) as c:
+ data = set([int(f['properties']['position']) for f in c])
+ assert len(positions) == len(data)
+ for p in positions:
+ assert p in data
+
+
+ at pytest.mark.parametrize('driver', [driver for driver in driver_mode_mingdal['w'].keys()
+ if _driver_supports_mode(driver, 'w')])
+def test_read_getfeaturecount(tmpdir, driver, testdata_generator):
+ """
+ Test if a call to OGR_L_GetFeatureCount has side effects for reading
+
+ """
+
+ schema, crs, records1, records2, test_equal, create_kwargs = testdata_generator(driver, range(0, 10), range(10, 20))
+ path = str(tmpdir.join(get_temp_filename(driver)))
+ positions = set([int(r['properties']['position']) for r in records1 + records2])
+
+ with fiona.open(path, 'w',
+ driver=driver,
+ crs=crs,
+ schema=schema,
+ **create_kwargs) as c:
+ c.writerecords(records1)
+ c.writerecords(records2)
+
+ with fiona.open(path) as c:
+ data = set()
+ for _ in range(len(records1)):
+ f = next(c)
+ data.add(int(f['properties']['position']))
+
+ # Call to OGR_L_GetFeatureCount
+ try:
+ assert len(data) == len(records1)
+ except TypeError:
+ pass
+
+ for _ in range(len(records1)):
+ f = next(c)
+ data.add(int(f['properties']['position']))
+
+ try:
+ assert len(data) == len(records1 + records2)
+ except TypeError:
+ pass
+
+ assert len(positions) == len(data)
+ for p in positions:
+ assert p in data
\ No newline at end of file
=====================================
tests/test_datetime.py
=====================================
@@ -13,7 +13,7 @@ from fiona.env import GDALVersion
import datetime
from fiona.drvsupport import (supported_drivers, driver_mode_mingdal, _driver_converts_field_type_silently_to_str,
_driver_supports_field, _driver_converts_to_str, _driver_supports_timezones,
- _driver_supports_milliseconds)
+ _driver_supports_milliseconds, _driver_supports_mode)
import pytz
from pytz import timezone
@@ -253,9 +253,7 @@ def generate_testcases():
for field_type in ['time', 'datetime', 'date']:
# Select only driver that are capable of writing fields
for driver, raw in supported_drivers.items():
- if ('w' in raw and
- (driver not in driver_mode_mingdal['w'] or
- gdal_version >= GDALVersion(*driver_mode_mingdal['w'][driver][:2]))):
+ if _driver_supports_mode(driver, 'w'):
if _driver_supports_field(driver, field_type):
if _driver_converts_field_type_silently_to_str(driver, field_type):
_test_cases_datefield_to_str.append((driver, field_type))
@@ -284,7 +282,7 @@ def test_datefield(tmpdir, driver, field_type):
elif field_type == 'datetime':
# some drivers do not support timezones. In this case, Fiona converts datetime fields with a timezone other
- # than UTC to UTC. Thus, both the dateime read by Fiona, as well as expected value are first converted to
+ # than UTC to UTC. Thus, both the datetime read by Fiona, as well as expected value are first converted to
# UTC before compared.
# Milliseconds
@@ -585,9 +583,8 @@ def test_datetime_field_type_marked_not_supported_is_not_supported(tmpdir, drive
"""
- if driver == "BNA" and gdal_version < GDALVersion(2, 0):
- # BNA driver segfaults with gdal 1.11
- return
+ if driver == "BNA" and GDALVersion.runtime() < GDALVersion(2, 0):
+ pytest.skip("BNA driver segfaults with gdal 1.11")
monkeypatch.delitem(fiona.drvsupport._driver_field_type_unsupported[field_type], driver)
@@ -621,8 +618,7 @@ def generate_tostr_testcases():
for field_type in _driver_converts_to_str:
for driver in _driver_converts_to_str[field_type]:
driver_supported = driver in supported_drivers
- driver_can_write = (driver not in driver_mode_mingdal['w'] or
- gdal_version >= GDALVersion(*driver_mode_mingdal['w'][driver][:2]))
+ driver_can_write = _driver_supports_mode(driver, 'w')
field_supported = _driver_supports_field(driver, field_type)
converts_to_str = _driver_converts_field_type_silently_to_str(driver, field_type)
if driver_supported and driver_can_write and converts_to_str and field_supported:
=====================================
tests/test_driver_options.py
=====================================
@@ -0,0 +1,30 @@
+import os
+import tempfile
+from collections import OrderedDict
+import glob
+import fiona
+from tests.conftest import get_temp_filename, requires_gdal2
+
+
+ at requires_gdal2
+def test_gml_format_option():
+ """ Test GML dataset creation option FORMAT (see https://github.com/Toblerity/Fiona/issues/968)"""
+
+ schema = {'geometry': 'Point', 'properties': OrderedDict([('position', 'int')])}
+ records = [{'geometry': {'type': 'Point', 'coordinates': (0.0, float(i))}, 'properties': {'position': i}} for i in
+ range(10)]
+
+ tmpdir = tempfile.mkdtemp()
+ fpath = os.path.join(tmpdir, get_temp_filename('GML'))
+
+ with fiona.open(fpath,
+ 'w',
+ driver="GML",
+ schema=schema,
+ FORMAT="GML3") as out:
+ out.writerecords(records)
+
+ xsd_path = glob.glob(os.path.join(tmpdir, "*.xsd"))[0]
+ with open(xsd_path) as f:
+ xsd = f.read()
+ assert "http://schemas.opengis.net/gml/3.1.1" in xsd
=====================================
tests/test_drvsupport.py
=====================================
@@ -5,6 +5,7 @@ from .conftest import requires_gdal24, get_temp_filename
from fiona.drvsupport import supported_drivers, driver_mode_mingdal
import fiona.drvsupport
from fiona.env import GDALVersion
+from fiona._env import calc_gdal_version_num, get_gdal_version_num
from fiona.errors import DriverError
@@ -23,30 +24,30 @@ def test_write_or_driver_error(tmpdir, driver, testdata_generator):
"""
if driver == "BNA" and GDALVersion.runtime() < GDALVersion(2, 0):
- # BNA driver segfaults with gdal 1.11
- return
+ pytest.skip("BNA driver segfaults with gdal 1.11")
- schema, crs, records1, _, test_equal = testdata_generator(driver, range(0, 10), [])
+ schema, crs, records1, _, test_equal, create_kwargs = testdata_generator(driver, range(0, 10), [])
path = str(tmpdir.join(get_temp_filename(driver)))
- if driver in driver_mode_mingdal['w'] and GDALVersion.runtime() < GDALVersion(
- *driver_mode_mingdal['w'][driver][:2]):
+ if (driver in driver_mode_mingdal['w'] and
+ get_gdal_version_num() < calc_gdal_version_num(*driver_mode_mingdal['w'][driver])):
# Test if DriverError is raised for gdal < driver_mode_mingdal
with pytest.raises(DriverError):
with fiona.open(path, 'w',
driver=driver,
crs=crs,
- schema=schema) as c:
+ schema=schema,
+ **create_kwargs) as c:
c.writerecords(records1)
else:
-
# Test if we can write
with fiona.open(path, 'w',
driver=driver,
crs=crs,
- schema=schema) as c:
+ schema=schema,
+ **create_kwargs) as c:
c.writerecords(records1)
@@ -73,21 +74,24 @@ def test_write_does_not_work_when_gdal_smaller_mingdal(tmpdir, driver, testdata_
"""
if driver == "BNA" and GDALVersion.runtime() < GDALVersion(2, 0):
- # BNA driver segfaults with gdal 1.11
- return
+ pytest.skip("BNA driver segfaults with gdal 1.11")
+ if (driver == 'FlatGeobuf' and
+ calc_gdal_version_num(3, 1, 0) <= get_gdal_version_num() < calc_gdal_version_num(3, 1, 3)):
+ pytest.skip("See https://github.com/Toblerity/Fiona/pull/924")
- schema, crs, records1, _, test_equal = testdata_generator(driver, range(0, 10), [])
+ schema, crs, records1, _, test_equal, create_kwargs = testdata_generator(driver, range(0, 10), [])
path = str(tmpdir.join(get_temp_filename(driver)))
- if driver in driver_mode_mingdal['w'] and GDALVersion.runtime() < GDALVersion(
- *driver_mode_mingdal['w'][driver][:2]):
+ if (driver in driver_mode_mingdal['w'] and
+ get_gdal_version_num() < calc_gdal_version_num(*driver_mode_mingdal['w'][driver])):
monkeypatch.delitem(fiona.drvsupport.driver_mode_mingdal['w'], driver)
with pytest.raises(Exception):
with fiona.open(path, 'w',
driver=driver,
crs=crs,
- schema=schema) as c:
+ schema=schema,
+ **create_kwargs) as c:
c.writerecords(records1)
@@ -100,27 +104,27 @@ def test_append_or_driver_error(tmpdir, testdata_generator, driver):
"""
if driver == "BNA" and GDALVersion.runtime() < GDALVersion(2, 0):
- # BNA driver segfaults with gdal 1.11
- return
+ pytest.skip("BNA driver segfaults with gdal 1.11")
path = str(tmpdir.join(get_temp_filename(driver)))
- schema, crs, records1, records2, test_equal = testdata_generator(driver, range(0, 5), range(5, 10))
+ schema, crs, records1, records2, test_equal, create_kwargs = testdata_generator(driver, range(0, 5), range(5, 10))
# If driver is not able to write, we cannot test append
- if driver in driver_mode_mingdal['w'] and GDALVersion.runtime() < GDALVersion(
- *driver_mode_mingdal['w'][driver][:2]):
+ if (driver in driver_mode_mingdal['w']
+ and get_gdal_version_num() < calc_gdal_version_num(*driver_mode_mingdal['w'][driver])):
return
# Create test file to append to
with fiona.open(path, 'w',
driver=driver,
crs=crs,
- schema=schema) as c:
+ schema=schema,
+ **create_kwargs) as c:
c.writerecords(records1)
- if driver in driver_mode_mingdal['a'] and GDALVersion.runtime() < GDALVersion(
- *driver_mode_mingdal['a'][driver][:2]):
+ if (driver in driver_mode_mingdal['a']
+ and get_gdal_version_num() < calc_gdal_version_num(*driver_mode_mingdal['a'][driver])):
# Test if DriverError is raised for gdal < driver_mode_mingdal
with pytest.raises(DriverError):
@@ -157,27 +161,27 @@ def test_append_does_not_work_when_gdal_smaller_mingdal(tmpdir, driver, testdata
"""
if driver == "BNA" and GDALVersion.runtime() < GDALVersion(2, 0):
- # BNA driver segfaults with gdal 1.11
- return
+ pytest.skip("BNA driver segfaults with gdal 1.11")
path = str(tmpdir.join(get_temp_filename(driver)))
- schema, crs, records1, records2, test_equal = testdata_generator(driver, range(0, 5), range(5, 10))
+ schema, crs, records1, records2, test_equal, create_kwargs = testdata_generator(driver, range(0, 5), range(5, 10))
# If driver is not able to write, we cannot test append
- if driver in driver_mode_mingdal['w'] and GDALVersion.runtime() < GDALVersion(
- *driver_mode_mingdal['w'][driver][:2]):
+ if (driver in driver_mode_mingdal['w']
+ and get_gdal_version_num() < calc_gdal_version_num(*driver_mode_mingdal['w'][driver])):
return
# Create test file to append to
with fiona.open(path, 'w',
driver=driver,
crs=crs,
- schema=schema) as c:
+ schema=schema,
+ **create_kwargs) as c:
c.writerecords(records1)
- if driver in driver_mode_mingdal['a'] and GDALVersion.runtime() < GDALVersion(
- *driver_mode_mingdal['a'][driver][:2]):
+ if (driver in driver_mode_mingdal['a']
+ and get_gdal_version_num() < calc_gdal_version_num(*driver_mode_mingdal['a'][driver])):
# Test if driver really can't append for gdal < driver_mode_mingdal
monkeypatch.delitem(fiona.drvsupport.driver_mode_mingdal['a'], driver)
@@ -208,7 +212,7 @@ def test_no_write_driver_cannot_write(tmpdir, driver, testdata_generator, monkey
"""
monkeypatch.setitem(fiona.drvsupport.supported_drivers, driver, 'rw')
- schema, crs, records1, _, test_equal = testdata_generator(driver, range(0, 5), [])
+ schema, crs, records1, _, test_equal, create_kwargs = testdata_generator(driver, range(0, 5), [])
if driver == "BNA" and GDALVersion.runtime() < GDALVersion(2, 0):
pytest.skip("BNA driver segfaults with gdal 1.11")
@@ -222,7 +226,8 @@ def test_no_write_driver_cannot_write(tmpdir, driver, testdata_generator, monkey
with fiona.open(path, 'w',
driver=driver,
crs=crs,
- schema=schema) as c:
+ schema=schema,
+ **create_kwargs) as c:
c.writerecords(records1)
@@ -239,22 +244,22 @@ def test_no_append_driver_cannot_append(tmpdir, driver, testdata_generator, monk
monkeypatch.setitem(fiona.drvsupport.supported_drivers, driver, 'raw')
if driver == "BNA" and GDALVersion.runtime() < GDALVersion(2, 0):
- # BNA driver segfaults with gdal 1.11
- return
+ pytest.skip("BNA driver segfaults with gdal 1.11")
path = str(tmpdir.join(get_temp_filename(driver)))
- schema, crs, records1, records2, test_equal = testdata_generator(driver, range(0, 5), range(5, 10))
+ schema, crs, records1, records2, test_equal, create_kwargs = testdata_generator(driver, range(0, 5), range(5, 10))
# If driver is not able to write, we cannot test append
- if driver in driver_mode_mingdal['w'] and GDALVersion.runtime() < GDALVersion(
- *driver_mode_mingdal['w'][driver][:2]):
+ if (driver in driver_mode_mingdal['w'] and
+ get_gdal_version_num() < calc_gdal_version_num(*driver_mode_mingdal['w'][driver])):
return
# Create test file to append to
with fiona.open(path, 'w',
driver=driver,
crs=crs,
- schema=schema) as c:
+ schema=schema,
+ **create_kwargs) as c:
c.writerecords(records1)
=====================================
tests/test_fio_load.py
=====================================
@@ -4,7 +4,6 @@
import json
import os
import shutil
-import sys
import pytest
@@ -34,7 +33,6 @@ def test_collection(tmpdir, feature_collection, runner):
assert len(fiona.open(tmpfile)) == 2
-
def test_seq_rs(feature_seq_pp_rs, tmpdir, runner):
tmpfile = str(tmpdir.mkdir('tests').join('test_seq_rs.shp'))
result = runner.invoke(
@@ -123,3 +121,15 @@ def test_fio_load_layer(tmpdir, runner):
finally:
shutil.rmtree(outdir)
+
+
+ at pytest.mark.iconv
+def test_creation_options(tmpdir, runner, feature_seq):
+ tmpfile = str(tmpdir.mkdir("tests").join("test.shp"))
+ result = runner.invoke(
+ main_group,
+ ["load", "-f", "Shapefile", "--co", "ENCODING=LATIN1", tmpfile],
+ feature_seq,
+ )
+ assert result.exit_code == 0
+ assert tmpdir.join("tests/test.cpg").read() == "LATIN1"
=====================================
tests/test_slice.py
=====================================
@@ -70,12 +70,17 @@ def slice_dataset_path(request):
schema = get_schema(driver)
records = get_records(driver, range(min_id, max_id + 1))
+ create_kwargs = {}
+ if driver == 'FlatGeobuf':
+ create_kwargs['SPATIAL_INDEX'] = False
+
tmpdir = tempfile.mkdtemp()
path = os.path.join(tmpdir, get_temp_filename(driver))
with fiona.open(path, 'w',
driver=driver,
- schema=schema) as c:
+ schema=schema,
+ **create_kwargs) as c:
c.writerecords(records)
yield path
shutil.rmtree(tmpdir)
=====================================
tests/test_transform.py
=====================================
@@ -70,3 +70,15 @@ def test_axis_ordering(crs):
geom = {"type": "Point", "coordinates": [-8427998.647958742, 4587905.27136252]}
g2 = transform.transform_geom("epsg:3857", crs, geom, precision=3)
assert g2["coordinates"] == pytest.approx(rev_expected)
+
+
+def test_transform_issue971():
+ """ See https://github.com/Toblerity/Fiona/issues/971 """
+ source_crs = "epsg:25832"
+ dest_src = "epsg:4326"
+ geom = {'type': 'GeometryCollection', 'geometries': [{'type': 'LineString',
+ 'coordinates': [(512381.8870945257, 5866313.311218272),
+ (512371.23869999964, 5866322.282500001),
+ (512364.6014999999, 5866328.260199999)]}]}
+ geom_transformed = transform.transform_geom(source_crs, dest_src, geom, precision=3)
+ assert geom_transformed['geometries'][0]['coordinates'][0] == pytest.approx((9.184, 52.946))
View it on GitLab: https://salsa.debian.org/debian-gis-team/fiona/-/commit/1c7d94a6021856b030e59077462586bb31bdf6f0
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/fiona/-/commit/1c7d94a6021856b030e59077462586bb31bdf6f0
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20201118/fa6048d1/attachment-0001.html>
More information about the Pkg-grass-devel
mailing list