From 2070b22065f49f3f306f694a838894e8b1ea880a Mon Sep 17 00:00:00 2001 From: Bas Couwenberg Date: Mon, 15 Dec 2025 14:34:00 +0100 Subject: [PATCH 1/7] Mark tests that require network or geotiff dependency. --- pytest.ini | 3 +++ tests/test_execute.py | 1 + tests/test_inout.py | 4 ++++ tests/test_ows.py | 3 +++ tests/validator/test_complexvalidators.py | 4 ++++ 5 files changed, 15 insertions(+) diff --git a/pytest.ini b/pytest.ini index 1f1f6bb94..90dab3952 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,3 +2,6 @@ addopts = --import-mode=importlib pythonpath = tests testpaths = tests +markers = + online: marks tests requiring network + geotiff: marks tests requiring geotiff module diff --git a/tests/test_execute.py b/tests/test_execute.py index bcc2f0a30..62aefd706 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -235,6 +235,7 @@ def get_output(doc): class ExecuteTest(TestBase): """Test for Exeucte request KVP request""" + @pytest.mark.online @pytest.mark.xfail(reason="test.opendap.org is offline") def test_dods(self): if not WITH_NC4: diff --git a/tests/test_inout.py b/tests/test_inout.py index 6d532892a..b4d027d00 100644 --- a/tests/test_inout.py +++ b/tests/test_inout.py @@ -15,6 +15,7 @@ import json from pywps import inout import base64 +import pytest from pywps import Format, FORMATS from pywps.app.Common import Metadata @@ -128,6 +129,7 @@ def test_file(self): with self.assertRaises(TypeError): self.iohandler[0].data = '5' + @pytest.mark.online def test_url(self): if not service_ok('https://demo.mapserver.org'): self.skipTest("mapserver is unreachable") @@ -552,6 +554,7 @@ def test_base64(self): b = self.complex_out.base64 self.assertEqual(base64.b64decode(b).decode(), self.data) + @pytest.mark.online def test_url_handler(self): wfsResource = 'http://demo.mapserver.org/cgi-bin/wfs?' \ 'service=WFS&version=1.1.0&' \ @@ -798,6 +801,7 @@ def test_json(self): ) +@pytest.mark.online class TestMetaLink(TestBase): def setUp(self) -> None: diff --git a/tests/test_ows.py b/tests/test_ows.py index de354c65d..188908054 100644 --- a/tests/test_ows.py +++ b/tests/test_ows.py @@ -14,6 +14,7 @@ from pywps import get_ElementMakerForVersion import pywps.configuration as config from pywps.tests import client_for, assert_response_success, service_ok +import pytest wfsResource = 'https://demo.mapserver.org/cgi-bin/wfs?service=WFS&version=1.1.0&request=GetFeature&typename=continents&maxfeatures=10' # noqa wcsResource = 'https://demo.mapserver.org/cgi-bin/wcs?service=WCS&version=1.0.0&request=GetCoverage&coverage=ndvi&crs=EPSG:4326&bbox=-92,42,-85,45&format=image/tiff&width=400&height=300' # noqa @@ -91,6 +92,7 @@ def sum_one(request, response): supported_formats=[get_format('GEOTIFF')])], grass_location='epsg:4326') + @pytest.mark.online def test_wfs(self): if not service_ok('https://demo.mapserver.org'): self.skipTest("mapserver is unreachable") @@ -117,6 +119,7 @@ def test_wfs(self): # . the inclusion of output # . the type of output + @pytest.mark.online def test_wcs(self): if not config.CONFIG.get('grass', 'gisbase'): self.skipTest('GRASS lib not found') diff --git a/tests/validator/test_complexvalidators.py b/tests/validator/test_complexvalidators.py index 032fe027e..badd81299 100644 --- a/tests/validator/test_complexvalidators.py +++ b/tests/validator/test_complexvalidators.py @@ -69,6 +69,7 @@ class data_format(object): return fake_input + @pytest.mark.online def test_gml_validator(self): """Test GML validator """ @@ -79,6 +80,7 @@ def test_gml_validator(self): # self.assertTrue(validategml(gml_input, MODE.VERYSTRICT), 'VERYSTRICT validation') gml_input.stream.close() + @pytest.mark.online @pytest.mark.xfail(reason="gml verystrict validation fails") def test_gml_validator_verystrict(self): """Test GML validator @@ -117,6 +119,7 @@ def test_shapefile_validator(self): self.assertTrue(validateshapefile(shapefile_input, MODE.STRICT), 'STRICT validation') shapefile_input.stream.close() + @pytest.mark.geotiff def test_geotiff_validator(self): """Test GeoTIFF validator """ @@ -141,6 +144,7 @@ def test_netcdf_validator(self): else: self.assertFalse(validatenetcdf(netcdf_input, MODE.STRICT), 'STRICT validation') + @pytest.mark.online @pytest.mark.xfail(reason="test.opendap.org is offline") def test_dods_validator(self): opendap_input = ComplexInput('dods', 'opendap test', [FORMATS.DODS,]) From 8de017439820cc78ed10c1398fc1a6fdf4de04a1 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:12:49 -0500 Subject: [PATCH 2/7] split extras requirements into another installation recipe --- environment.yml | 5 ++++- requirements-extra.txt | 4 ++++ requirements.txt | 4 ---- setup.py | 4 ++++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/environment.yml b/environment.yml index 552d89a07..8d234e5e2 100644 --- a/environment.yml +++ b/environment.yml @@ -12,9 +12,12 @@ dependencies: - urllib3 >=2.5.0 - markupsafe >=3.0.3 - numpy >=1.22.2 - - zarr <3 + # extras - fiona - geotiff + - netcdf4 + - tifffile <=2025.5.10 + - zarr <3.0 # tests - pytest - ruff >=0.5.7 diff --git a/requirements-extra.txt b/requirements-extra.txt index 7d438f194..ba5216b87 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -1 +1,5 @@ netCDF4 +fiona +geotiff +tifffile<=2025.5.10 +zarr<3.0 diff --git a/requirements.txt b/requirements.txt index dc85b1c08..595a84f2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,5 @@ markupsafe sqlalchemy -fiona -geotiff -tifffile <=2025.5.10 -zarr <3 humanize jinja2 jsonschema diff --git a/setup.py b/setup.py index b280f852a..235a7973e 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,9 @@ with open("requirements-dev.txt") as frd: DEV_REQUIRES = frd.read().splitlines() +with open("requirements-extras.txt") as frd: + EXTRAS_REQUIRES = frd.read().splitlines() + CONFIG = { "name": "pywps", "version": VERSION, @@ -58,6 +61,7 @@ "install_requires": INSTALL_REQUIRES, "extras_require": dict( dev=DEV_REQUIRES, + extras=EXTRAS_REQUIRES, ), "python_requires": ">=3.10,<4", "packages": find_packages(exclude=["docs", "tests.*", "tests"]), From e6c2e75a00155cdfadc7bf558cdcb4c457385e74 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:13:31 -0500 Subject: [PATCH 3/7] better marker control on tests, conditional running of tests dependent on extras --- pytest.ini | 8 +- tests/{test_assync.py => test_async.py} | 4 - ...st_assync_inout.py => test_async_inout.py} | 0 tests/test_execute.py | 15 +- tests/test_s3storage.py | 4 +- tests/validator/test_complexvalidators.py | 142 ++++++++++++++---- 6 files changed, 126 insertions(+), 47 deletions(-) rename tests/{test_assync.py => test_async.py} (95%) rename tests/{test_assync_inout.py => test_async_inout.py} (100%) diff --git a/pytest.ini b/pytest.ini index 90dab3952..17d18719a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,11 @@ [pytest] -addopts = --import-mode=importlib +addopts = + --import-mode=importlib +xfail_strict = true pythonpath = tests testpaths = tests markers = online: marks tests requiring network - geotiff: marks tests requiring geotiff module + requires_fiona: marks tests requiring fiona module + requires_geotiff: marks tests requiring geotiff module + requires_netcdf4: marks tests requiring netcdf4 module diff --git a/tests/test_assync.py b/tests/test_async.py similarity index 95% rename from tests/test_assync.py rename to tests/test_async.py index 38c39cb01..8797e70e4 100644 --- a/tests/test_assync.py +++ b/tests/test_async.py @@ -12,8 +12,6 @@ from processes import Sleep from owslib.wps import WPSExecution from pathlib import Path -from tempfile import TemporaryDirectory -from pywps import dblog VERSION = "1.0.0" @@ -27,7 +25,6 @@ def setUp(self) -> None: # Running processes using the MultiProcessing scheduler and a file-based database configuration.CONFIG.set('processing', 'mode', 'distributed') - @pytest.mark.xfail(reason="async fails") def test_async(self): client = client_for(Service(processes=[Sleep()])) wps = WPSExecution() @@ -48,7 +45,6 @@ def test_async(self): # Parse response to extract the status file path url = resp.xml.xpath("//@statusLocation")[0] - print(url) # OWSlib only reads from URLs, not local files. So we need to read the response manually. p = Path(configuration.get_config_value('server', 'outputpath')) / url.split('/')[-1] diff --git a/tests/test_assync_inout.py b/tests/test_async_inout.py similarity index 100% rename from tests/test_assync_inout.py rename to tests/test_async_inout.py diff --git a/tests/test_execute.py b/tests/test_execute.py index 62aefd706..c9611d367 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -22,12 +22,11 @@ from io import StringIO +netCDF4 = None try: import netCDF4 except ImportError: - WITH_NC4 = False -else: - WITH_NC4 = True + pass DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data') @@ -236,10 +235,9 @@ class ExecuteTest(TestBase): """Test for Exeucte request KVP request""" @pytest.mark.online - @pytest.mark.xfail(reason="test.opendap.org is offline") + @pytest.mark.requires_netcdf4 + @pytest.mark.skipif(netCDF4 is None, reason='netCDF4 libraries are required for this test') def test_dods(self): - if not WITH_NC4: - self.skipTest('netCDF4 not installed') my_process = create_complex_nc_process() service = Service(processes=[my_process]) @@ -282,8 +280,11 @@ class FakeRequest(): language = "en-US" request = FakeRequest() - resp = service.execute('my_opendap_process', request, 'fakeuuid') + + if resp.outputs["conventions"].data is None: + pytest.xfail("Network is likely unavailable or test.opendap.org is offline") + self.assertEqual(resp.outputs['conventions'].data, 'CF-1.0') self.assertEqual(resp.outputs['outdods'].url, href) self.assertTrue(resp.outputs['outdods'].as_reference) diff --git a/tests/test_s3storage.py b/tests/test_s3storage.py index 8ee694fab..448ff9f1c 100644 --- a/tests/test_s3storage.py +++ b/tests/test_s3storage.py @@ -4,7 +4,7 @@ ################################################################## from basic import TestBase -from pywps.inout.storage.s3 import S3StorageBuilder, S3Storage +from pywps.inout.storage.s3 import S3StorageBuilder from pywps.inout.storage import STORE_TYPE from pywps.inout.basic import ComplexOutput @@ -41,7 +41,7 @@ def test_write(self, uploadData): configuration.CONFIG.set('s3', 'prefix', 'wps') storage = S3StorageBuilder().build() - url = storage.write('Bar Baz', 'out.txt', data_format=FORMATS.TEXT) + storage.write('Bar Baz', 'out.txt', data_format=FORMATS.TEXT) called_args = uploadData.call_args[0] diff --git a/tests/validator/test_complexvalidators.py b/tests/validator/test_complexvalidators.py index badd81299..daec1b01b 100644 --- a/tests/validator/test_complexvalidators.py +++ b/tests/validator/test_complexvalidators.py @@ -27,12 +27,23 @@ import os +netCDF4 = None try: - import netCDF4 # noqa + import netCDF4 except ImportError: - WITH_NC4 = False -else: - WITH_NC4 = True + pass + +geotiff = None +try: + import geotiff +except ImportError: + pass + +fiona = None +try: + import fiona +except (ImportError, ModuleNotFoundError): + pass class ValidateTest(TestBase): @@ -70,9 +81,10 @@ class data_format(object): return fake_input @pytest.mark.online + @pytest.mark.requires_fiona + @pytest.mark.skipif(fiona is None, reason="fiona libraries are required for this test") def test_gml_validator(self): - """Test GML validator - """ + """Test GML validator""" gml_input = self.get_input('gml/point.gml', 'point.xsd', FORMATS.GML.mime_type) self.assertTrue(validategml(gml_input, MODE.NONE), 'NONE validation') self.assertTrue(validategml(gml_input, MODE.SIMPLE), 'SIMPLE validation') @@ -81,26 +93,39 @@ def test_gml_validator(self): gml_input.stream.close() @pytest.mark.online + @pytest.mark.skipif(fiona is not None, reason="fiona libraries must not be installed for this test") + def test_no_gml_validator(self): + """Test GML validator""" + gml_input = self.get_input('gml/point.gml', 'point.xsd', FORMATS.GML.mime_type) + self.assertTrue(validategml(gml_input, MODE.NONE), 'NONE validation') + self.assertTrue(validategml(gml_input, MODE.SIMPLE), 'SIMPLE validation') + self.assertFalse(validategml(gml_input, MODE.STRICT), 'STRICT validation') + # self.assertTrue(validategml(gml_input, MODE.VERYSTRICT), 'VERYSTRICT validation') + gml_input.stream.close() + + @pytest.mark.online + @pytest.mark.requires_fiona @pytest.mark.xfail(reason="gml verystrict validation fails") + @pytest.mark.skipif(fiona is None, reason="fiona libraries are required for this test") def test_gml_validator_verystrict(self): - """Test GML validator - """ + """Test GML validator""" gml_input = self.get_input('gml/point.gml', 'point.xsd', FORMATS.GML.mime_type) self.assertTrue(validategml(gml_input, MODE.VERYSTRICT), 'VERYSTRICT validation') gml_input.stream.close() + def test_json_validator(self): - """Test GeoJSON validator - """ + """Test GeoJSON validator""" json_input = self.get_input('json/point.geojson', None, FORMATS.JSON.mime_type) self.assertTrue(validatejson(json_input, MODE.NONE), 'NONE validation') self.assertTrue(validatejson(json_input, MODE.SIMPLE), 'SIMPLE validation') self.assertTrue(validatejson(json_input, MODE.STRICT), 'STRICT validation') json_input.stream.close() + @pytest.mark.requires_fiona + @pytest.mark.skipif(fiona is None, reason="fiona libraries are required for this test") def test_geojson_validator(self): - """Test GeoJSON validator - """ + """Test GeoJSON validator""" geojson_input = self.get_input('json/point.geojson', 'json/schema/geojson.json', FORMATS.GEOJSON.mime_type) self.assertTrue(validategeojson(geojson_input, MODE.NONE), 'NONE validation') @@ -109,9 +134,26 @@ def test_geojson_validator(self): self.assertTrue(validategeojson(geojson_input, MODE.VERYSTRICT), 'VERYSTRICT validation') geojson_input.stream.close() + + @pytest.mark.skipif(fiona is not None, reason="fiona libraries must not be installed for this test") + def test_no_geojson_validator(self): + """Test GeoJSON validator""" + geojson_input = self.get_input('json/point.geojson', 'json/schema/geojson.json', + FORMATS.GEOJSON.mime_type) + self.assertTrue(validategeojson(geojson_input, MODE.NONE), 'NONE validation') + self.assertTrue(validategeojson(geojson_input, MODE.SIMPLE), 'SIMPLE validation') + + self.assertFalse(validategeojson(geojson_input, MODE.STRICT), 'STRICT validation') + + # FIXME: MODE.VERYSTRICT should fail here + self.assertTrue(validategeojson(geojson_input, MODE.VERYSTRICT), 'VERYSTRICT validation') + + geojson_input.stream.close() + + @pytest.mark.requires_fiona + @pytest.mark.skipif(fiona is None, reason="fiona libraries are required for this test") def test_shapefile_validator(self): - """Test ESRI Shapefile validator - """ + """Test ESRI Shapefile validator""" shapefile_input = self.get_input('shp/point.shp.zip', None, FORMATS.SHP.mime_type) self.assertTrue(validateshapefile(shapefile_input, MODE.NONE), 'NONE validation') @@ -119,10 +161,30 @@ def test_shapefile_validator(self): self.assertTrue(validateshapefile(shapefile_input, MODE.STRICT), 'STRICT validation') shapefile_input.stream.close() - @pytest.mark.geotiff + @pytest.mark.skipif(fiona is not None, reason="fiona libraries must not be installed for this test") + def test_no_shapefile_validator(self): + """Test ESRI Shapefile validator""" + shapefile_input = self.get_input('shp/point.shp.zip', None, + FORMATS.SHP.mime_type) + self.assertTrue(validateshapefile(shapefile_input, MODE.NONE), 'NONE validation') + self.assertTrue(validateshapefile(shapefile_input, MODE.SIMPLE), 'SIMPLE validation') + self.assertFalse(validateshapefile(shapefile_input, MODE.STRICT), 'STRICT validation') + shapefile_input.stream.close() + + @pytest.mark.skipif(geotiff is not None, reason="geotiff libraries must not be installed for this test") + def test_no_geotiff_validator(self): + """Test GeoTIFF validator""" + geotiff_input = self.get_input('geotiff/dem.tiff', None, + FORMATS.GEOTIFF.mime_type) + self.assertTrue(validategeotiff(geotiff_input, MODE.NONE), 'NONE validation') + self.assertTrue(validategeotiff(geotiff_input, MODE.SIMPLE), 'SIMPLE validation') + self.assertFalse(validategeotiff(geotiff_input, MODE.STRICT), 'STRICT validation') + geotiff_input.stream.close() + + @pytest.mark.requires_geotiff + @pytest.mark.skipif(geotiff is None, reason="geotiff libraries are required for this test") def test_geotiff_validator(self): - """Test GeoTIFF validator - """ + """Test GeoTIFF validator""" geotiff_input = self.get_input('geotiff/dem.tiff', None, FORMATS.GEOTIFF.mime_type) self.assertTrue(validategeotiff(geotiff_input, MODE.NONE), 'NONE validation') @@ -130,40 +192,56 @@ def test_geotiff_validator(self): self.assertTrue(validategeotiff(geotiff_input, MODE.STRICT), 'STRICT validation') geotiff_input.stream.close() + @pytest.mark.requires_netcdf4 + @pytest.mark.skipif(netCDF4 is None, reason="NetCDF4 libraries are required for this test") def test_netcdf_validator(self): - """Test netCDF validator - """ + """Test netCDF validator""" netcdf_input = self.get_input('netcdf/time.nc', None, FORMATS.NETCDF.mime_type) self.assertTrue(validatenetcdf(netcdf_input, MODE.NONE), 'NONE validation') self.assertTrue(validatenetcdf(netcdf_input, MODE.SIMPLE), 'SIMPLE validation') netcdf_input.stream.close() - if WITH_NC4: - self.assertTrue(validatenetcdf(netcdf_input, MODE.STRICT), 'STRICT validation') - netcdf_input.file = 'grub.nc' - self.assertFalse(validatenetcdf(netcdf_input, MODE.STRICT)) - else: - self.assertFalse(validatenetcdf(netcdf_input, MODE.STRICT), 'STRICT validation') + + self.assertTrue(validatenetcdf(netcdf_input, MODE.STRICT), 'STRICT validation') + netcdf_input.file = 'grub.nc' + self.assertFalse(validatenetcdf(netcdf_input, MODE.STRICT)) + + @pytest.mark.skipif(netCDF4 is not None, reason="NetCDF4 libraries must not be installed for this test") + def test_no_netcdf_validator(self): + """Test netCDF validator""" + netcdf_input = self.get_input('netcdf/time.nc', None, FORMATS.NETCDF.mime_type) + self.assertTrue(validatenetcdf(netcdf_input, MODE.NONE), 'NONE validation') + self.assertTrue(validatenetcdf(netcdf_input, MODE.SIMPLE), 'SIMPLE validation') + netcdf_input.stream.close() + + self.assertFalse(validatenetcdf(netcdf_input, MODE.STRICT), 'STRICT validation') @pytest.mark.online - @pytest.mark.xfail(reason="test.opendap.org is offline") + @pytest.mark.requires_netcdf4 + @pytest.mark.skipif(netCDF4 is None, reason="NetCDF4 libraries are required for this test") def test_dods_validator(self): opendap_input = ComplexInput('dods', 'opendap test', [FORMATS.DODS,]) opendap_input.url = "http://test.opendap.org:80/opendap/netcdf/examples/sresa1b_ncar_ccsm3_0_run1_200001.nc" self.assertTrue(validatedods(opendap_input, MODE.NONE), 'NONE validation') self.assertTrue(validatedods(opendap_input, MODE.SIMPLE), 'SIMPLE validation') - if WITH_NC4: - self.assertTrue(validatedods(opendap_input, MODE.STRICT), 'STRICT validation') - opendap_input.url = 'Faulty url' - self.assertFalse(validatedods(opendap_input, MODE.STRICT)) - else: - self.assertFalse(validatedods(opendap_input, MODE.STRICT), 'STRICT validation') + self.assertTrue(validatedods(opendap_input, MODE.STRICT), 'STRICT validation') + opendap_input.url = 'Faulty url' + self.assertFalse(validatedods(opendap_input, MODE.STRICT)) + @pytest.mark.online + @pytest.mark.skipif(netCDF4 is not None, reason="NetCDF4 libraries must not be installed for this test") def test_dods_default(self): opendap_input = ComplexInput('dods', 'opendap test', [FORMATS.DODS,], default='http://test.opendap.org', default_type=SOURCE_TYPE.URL, mode=MODE.SIMPLE) + opendap_input.url = "http://test.opendap.org:80/opendap/netcdf/examples/sresa1b_ncar_ccsm3_0_run1_200001.nc" + self.assertTrue(validatedods(opendap_input, MODE.NONE), 'NONE validation') + self.assertTrue(validatedods(opendap_input, MODE.SIMPLE), 'SIMPLE validation') + + with pytest.warns(UserWarning) as record: + self.assertFalse(validatedods(opendap_input, MODE.STRICT), 'STRICT validation') + assert "Complex validation requires netCDF4 support." in record[0].message.args[0] def test_fail_validator(self): fake_input = self.get_input('point.xsd', 'point.xsd', FORMATS.SHP.mime_type) From 89f3c25523be49944c22f83a6f8526c9010b5a90 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:15:57 -0500 Subject: [PATCH 4/7] typo fix --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 235a7973e..5c259908a 100644 --- a/setup.py +++ b/setup.py @@ -25,8 +25,8 @@ with open("requirements-dev.txt") as frd: DEV_REQUIRES = frd.read().splitlines() -with open("requirements-extras.txt") as frd: - EXTRAS_REQUIRES = frd.read().splitlines() +with open("requirements-extra.txt") as frd: + EXTRA_REQUIRES = frd.read().splitlines() CONFIG = { "name": "pywps", @@ -61,7 +61,7 @@ "install_requires": INSTALL_REQUIRES, "extras_require": dict( dev=DEV_REQUIRES, - extras=EXTRAS_REQUIRES, + extra=EXTRA_REQUIRES, ), "python_requires": ">=3.10,<4", "packages": find_packages(exclude=["docs", "tests.*", "tests"]), From c0aa7db99b502e73c3839b09fc18eadc1d2db047 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:18:01 -0500 Subject: [PATCH 5/7] typo fix --- tests/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 560137a2f..93fcc72b8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -27,7 +27,7 @@ import test_service import test_process import test_processing -import test_assync +import test_async import test_grass_location import test_storage import test_filestorage @@ -92,7 +92,7 @@ def load_tests(loader=None, tests=None, pattern=None): test_service.load_tests(), test_process.load_tests(), test_processing.load_tests(), - test_assync.load_tests(), + test_async.load_tests(), test_grass_location.load_tests(), test_storage.load_tests(), test_filestorage.load_tests(), From 1c2f1ac097ca6683e8498b87cc0461f39cf52460 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:03:30 -0500 Subject: [PATCH 6/7] remove coveralls python library, use tox-gh, update actions versions, parallel coverage reporting --- .github/workflows/main.yml | 80 ++++++++++++++++++++++---------------- requirements-dev.txt | 3 +- tox.ini | 21 ++++++---- 3 files changed, 61 insertions(+), 43 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fd2e4f3b7..9babd0ef4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,22 +6,23 @@ on: - main pull_request: +concurrency: + # For a given workflow, if we push to the same branch, cancel all previous builds on that branch except on main. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + jobs: lint: name: Linting Suite runs-on: ubuntu-latest steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.11.0 - with: - access_token: ${{ github.token }} - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install tox run: | - pip install tox>=4.0 + pip install tox>=4.30.3 - name: Run linting suite ⚙️ run: | tox -e lint @@ -32,40 +33,37 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - include: - - tox-env: py310-extra - python-version: "3.10" - - tox-env: py311-extra - python-version: "3.11" - - tox-env: py312-extra - python-version: "3.12" - - tox-env: py313-extra - python-version: "3.13" + python-version: [ "3.10", "3.11", "3.12", "3.13" ] steps: - - uses: actions/checkout@v4 - - name: Install packages 📦 - run: | - sudo apt-get update - sudo apt-get -y install libnetcdf-dev libhdf5-dev - - uses: actions/setup-python@v5 - name: Setup Python ${{ matrix.python-version }} - with: - python-version: ${{ matrix.python-version }} - - name: Install tox 📦 - run: pip install "tox>=4.0" - - name: Run tests with tox ⚙️ - run: tox -e ${{ matrix.tox-env }} - - name: Run coveralls ⚙️ - if: matrix.python-version == 3.10 - uses: AndreMiras/coveralls-python-action@develop + - uses: actions/checkout@v6 + - name: Install packages 📦 + run: | + sudo apt-get update + sudo apt-get -y install libnetcdf-dev libhdf5-dev + - uses: actions/setup-python@v6 + name: Setup Python ${{ matrix.python-version }} + with: + python-version: ${{ matrix.python-version }} + - name: Install tox 📦 + run: pip install "tox>=4.30.3" "tox-gh>=1.5" + - name: Run tests with tox ⚙️ + run: | + tox + env: + TOX_GH_MAJOR_MINOR: ${{ matrix.python-version }} + - name: Report Coverage + uses: coverallsapp/github-action@v2 + with: + flag-name: run-${{ matrix.python-version }} + parallel: true docs: name: Build docs 🏗️ needs: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 name: Setup Python 3.10 with: python-version: "3.10" @@ -73,3 +71,17 @@ jobs: run: | pip install -e .[dev] cd docs && make html + + finish: + name: Finish + needs: + - test + - docs + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true diff --git a/requirements-dev.txt b/requirements-dev.txt index e7c0b4657..88cdcc0df 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,12 +1,11 @@ bump2version coverage -coveralls docutils ruff pylint pytest pytest-cov sphinx -tox>=4.0 +tox>=4.30.3 twine wheel diff --git a/tox.ini b/tox.ini index b8d0b61df..5a1f9f163 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,17 @@ [tox] -min_version = 4.0 +min_version = 4.30.3 envlist = py{310,311,312,313}{-extra,}, lint -requires = pip >=25.2 opts = --verbose +[gh] +python = + 3.10 = py3.10 + 3.11 = py3.11-extra + 3.12 = py3.12 + 3.13 = py3.13-extra + [testenv:lint] skip_install = true extras = @@ -16,7 +22,7 @@ commands = [testenv] setenv = - PYTEST_ADDOPTS = "--color=yes" + PYTEST_ADDOPTS = "--color=yes" "--cov=pywps" "--cov-report=lcov" PYTHONPATH = {toxinidir} COV_CORE_SOURCE = passenv = @@ -26,10 +32,11 @@ passenv = download = True install_command = python -m pip install --no-user {opts} {packages} -extras = dev -deps = - extra: -rrequirements-extra.txt +extras = + dev + extra: extra commands = ; # magic for gathering the GDAL version within tox ; sh -c 'pip install GDAL=="$(gdal-config --version)" --global-option=build_ext --global-option="-I/usr/include/gdal"' - pytest --cov + pytest {posargs} + coverage report From e633edc0dc6baffe6a5cc59e15a2ad52c997ba49 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:10:32 -0500 Subject: [PATCH 7/7] use caching, ignore lcov file --- .github/workflows/main.yml | 3 +++ .gitignore | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9babd0ef4..98433f656 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,6 +20,7 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.10" + cache: 'pip' - name: Install tox run: | pip install tox>=4.30.3 @@ -44,6 +45,7 @@ jobs: name: Setup Python ${{ matrix.python-version }} with: python-version: ${{ matrix.python-version }} + cache: 'pip' - name: Install tox 📦 run: pip install "tox>=4.30.3" "tox-gh>=1.5" - name: Run tests with tox ⚙️ @@ -67,6 +69,7 @@ jobs: name: Setup Python 3.10 with: python-version: "3.10" + cache: 'pip' - name: Build documentation 🏗️ run: | pip install -e .[dev] diff --git a/.gitignore b/.gitignore index 123e0315a..56fc153ec 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ docs/_build *.orig .coverage .pytest_cache +coverage.lcov \ No newline at end of file