diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index daf46852a..770eb2460 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -106,6 +106,8 @@ jobs: run: shell: bash timeout-minutes: 10 + env: + PYTHON_VERSION: 3.8 steps: - name: Checkout repo @@ -114,7 +116,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' cache-dependency-path: pyproject.toml @@ -122,15 +124,14 @@ jobs: run: | pip install --upgrade pip pip install . - pip install ".[test, optional]" - + pip install ".[test,optional]" + - name: Install Modflow executables uses: modflowpy/install-modflow-action@v1 - - - name: Run smoke tests - working-directory: ./autotest - run: | - pytest -v -n=auto --smoke --durations=0 --keep-failed=.failed + + - name: Smoke test + working-directory: autotest + run: pytest -v -n=auto --smoke --cov=flopy --cov-report=xml --durations=0 --keep-failed=.failed env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -138,9 +139,14 @@ jobs: uses: actions/upload-artifact@v3 if: failure() with: - name: failed-smoke-${{ matrix.os }}-${{ matrix.python-version }} - path: | - ./autotest/.failed/** + name: failed-smoke-${{ runner.os }}-${{ env.PYTHON_VERSION }} + path: ./autotest/.failed/** + + - name: Upload coverage + if: github.repository_owner == 'modflowpy' && (github.event_name == 'push' || github.event_name == 'pull_request') + uses: codecov/codecov-action@v3 + with: + files: ./autotest/coverage.xml test: name: Test @@ -220,7 +226,7 @@ jobs: if: runner.os != 'Windows' working-directory: ./autotest run: | - pytest -v -m="not example and not regression" -n=auto --cov=flopy --cov-report=xml --durations=0 --keep-failed=.failed --dist loadfile + pytest -v -m="not example and not regression" -n=auto --cov=flopy --cov-append --cov-report=xml --durations=0 --keep-failed=.failed --dist loadfile coverage report env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -230,7 +236,7 @@ jobs: shell: bash -l {0} working-directory: ./autotest run: | - pytest -v -m="not example and not regression" -n=auto --cov=flopy --cov-report=xml --durations=0 --keep-failed=.failed --dist loadfile + pytest -v -m="not example and not regression" -n=auto --cov=flopy --cov-append --cov-report=xml --durations=0 --keep-failed=.failed --dist loadfile coverage report env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -244,8 +250,7 @@ jobs: ./autotest/.failed/** - name: Upload coverage - if: - github.repository_owner == 'modflowpy' && (github.event_name == 'push' || github.event_name == 'pull_request') + if: github.repository_owner == 'modflowpy' && (github.event_name == 'push' || github.event_name == 'pull_request') uses: codecov/codecov-action@v3 with: files: ./autotest/coverage.xml diff --git a/.github/workflows/optional.yml b/.github/workflows/optional.yml new file mode 100644 index 000000000..094c92e6c --- /dev/null +++ b/.github/workflows/optional.yml @@ -0,0 +1,75 @@ +name: FloPy optional dependency testing +on: + schedule: + - cron: '0 8 * * *' # run at 8 AM UTC (12 am PST) +jobs: + test: + name: Test + runs-on: ubuntu-latest + defaults: + run: + shell: bash + timeout-minutes: 10 + env: + PYTHON_VERSION: 3.8 + strategy: + fail-fast: false + matrix: + optdeps: + # - "all optional dependencies" + - "no optional dependencies" + - "some optional dependencies" + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + cache-dependency-path: pyproject.toml + + - name: Install Python dependencies + run: | + pip install --upgrade pip + pip install . + pip install ".[test]" + + # Install optional dependencies according to matrix.optdeps. + # If matrix.optdeps is "some" remove 3 optional dependencies + # selected randomly using the current date as the seed. + if [[ ! "${{ matrix.optdeps }}" == *"no"* ]]; then + pip install ".[optional]" + fi + if [[ "${{ matrix.optdeps }}" == *"some"* ]]; then + deps=$(sed '/optional =/,/]/!d' pyproject.toml | sed -e '1d;$d' -e 's/\"//g' -e 's/,//g' | tr -d ' ' | cut -f 1 -d ';') + rmvd=$(echo $deps | tr ' ' '\n' | shuf --random-source <(yes date +%d.%m.%y) | head -n 3) + echo "Removing optional dependencies: $rmvd" >> removed_dependencies.txt + cat removed_dependencies.txt + pip uninstall --yes $rmvd + fi + + - name: Upload removed dependencies log + uses: actions/upload-artifact@v3 + with: + name: smoke-test-removed-dependencies + path: ./removed_dependencies.txt + + - name: Install Modflow executables + uses: modflowpy/install-modflow-action@v1 + + - name: Smoke test (${{ matrix.optdeps }}) + working-directory: autotest + run: pytest -v -n=auto --smoke --cov=flopy --cov-report=xml --durations=0 --keep-failed=.failed + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload failed test outputs + uses: actions/upload-artifact@v3 + if: failure() + with: + name: failed-smoke-${{ runner.os }}-${{ env.PYTHON_VERSION }} + path: ./autotest/.failed/** + \ No newline at end of file diff --git a/autotest/test_export.py b/autotest/test_export.py index 51e238f39..afe9bdf39 100644 --- a/autotest/test_export.py +++ b/autotest/test_export.py @@ -52,6 +52,11 @@ from flopy.utils.geometry import Polygon +HAS_PYPROJ = has_pkg("pyproj", strict=True) +if HAS_PYPROJ: + import pyproj + + def namfiles() -> List[Path]: mf2005_path = get_example_data_path() / "mf2005_test" return list(mf2005_path.rglob("*.nam")) @@ -330,10 +335,7 @@ def test_write_gridlines_shapefile(function_tmpdir): for suffix in [".dbf", ".shp", ".shx"]: assert outshp.with_suffix(suffix).exists() - if has_pkg("pyproj"): - assert outshp.with_suffix(".prj").exists() - else: - assert not outshp.with_suffix(".prj").exists() + assert outshp.with_suffix(".prj").exists() == HAS_PYPROJ with shapefile.Reader(str(outshp)) as sf: assert sf.shapeType == shapefile.POLYLINE @@ -1378,7 +1380,7 @@ def test_vtk_unstructured(function_tmpdir, example_data_path): assert np.allclose(np.ravel(top), top2), "Field data not properly written" -@requires_pkg("pyvista") +@requires_pkg("vtk", "pyvista") def test_vtk_to_pyvista(function_tmpdir, example_data_path): from autotest.test_mp7_cases import Mp7Cases diff --git a/autotest/test_grid.py b/autotest/test_grid.py index c3dafe555..214018777 100644 --- a/autotest/test_grid.py +++ b/autotest/test_grid.py @@ -1,6 +1,7 @@ import os import re import warnings +from contextlib import nullcontext from warnings import warn import matplotlib @@ -22,7 +23,7 @@ from flopy.utils.triangle import Triangle from flopy.utils.voronoi import VoronoiGrid -HAS_PYPROJ = has_pkg("pyproj") +HAS_PYPROJ = has_pkg("pyproj", strict=True) if HAS_PYPROJ: import pyproj @@ -594,9 +595,14 @@ def do_checks(g): do_checks(UnstructuredGrid(**d, crs=crs)) do_checks(VertexGrid(vertices=d["vertices"], crs=crs)) + # only check deprecations if pyproj is available + pyproj_avail_context = ( + pytest.deprecated_call() if HAS_PYPROJ else nullcontext() + ) + # test deprecated 'epsg' parameter if isinstance(crs, int): - with pytest.deprecated_call(): + with pyproj_avail_context: do_checks(StructuredGrid(delr=delr, delc=delc, epsg=crs)) if HAS_PYPROJ and crs == 26916: @@ -612,14 +618,14 @@ def do_checks(g): do_checks(StructuredGrid(delr=delr, delc=delc, prjfile=prjfile)) # test deprecated 'prj' parameter - with pytest.deprecated_call(): + with pyproj_avail_context: do_checks(StructuredGrid(delr=delr, delc=delc, prj=prjfile)) # test deprecated 'proj4' parameter with warnings.catch_warnings(): warnings.simplefilter("ignore") # pyproj warning about conversion proj4 = crs_obj.to_proj4() - with pytest.deprecated_call(): + with pyproj_avail_context: do_checks(StructuredGrid(delr=delr, delc=delc, proj4=proj4)) @@ -675,9 +681,14 @@ def do_checks(g, *, exp_srs=expected_srs, exp_epsg=expected_epsg): sg.set_coord_info(crs=26915, merge_coord_info=False) do_checks(sg, exp_srs="EPSG:26915", exp_epsg=26915) + # only check deprecations if pyproj is available + pyproj_avail_context = ( + pytest.deprecated_call() if HAS_PYPROJ else nullcontext() + ) + # test deprecated 'epsg' parameter if isinstance(crs, int): - with pytest.deprecated_call(): + with pyproj_avail_context: sg.set_coord_info(epsg=crs) do_checks(sg) @@ -827,8 +838,9 @@ def test_grid_crs_exceptions(): # test non-existing file not_a_file = "not-a-file" - with pytest.raises(FileNotFoundError): - StructuredGrid(delr=delr, delc=delc, prjfile=not_a_file) + if HAS_PYPROJ: + with pytest.raises(FileNotFoundError): + StructuredGrid(delr=delr, delc=delc, prjfile=not_a_file) # note "sg.prjfile = not_a_file" intentionally does not raise anything # test unhandled keyword diff --git a/autotest/test_gridgen.py b/autotest/test_gridgen.py index 2453e473f..0703a274e 100644 --- a/autotest/test_gridgen.py +++ b/autotest/test_gridgen.py @@ -14,6 +14,7 @@ from flopy.utils.gridgen import Gridgen +@requires_exe("gridgen") def test_ctor_accepts_path_or_string(function_tmpdir): grid = GridCases().structured_small() diff --git a/autotest/test_gridintersect.py b/autotest/test_gridintersect.py index 82c1f9385..498320e53 100644 --- a/autotest/test_gridintersect.py +++ b/autotest/test_gridintersect.py @@ -14,7 +14,7 @@ from flopy.utils.gridintersect import GridIntersect from flopy.utils.triangle import Triangle -if has_pkg("shapely"): +if has_pkg("shapely", strict=True): from shapely.geometry import ( LineString, MultiLineString, @@ -1221,7 +1221,7 @@ def test_polygon_offset_rot_structured_grid_shapely(rtree): # %% test rasters -@requires_pkg("rasterstats", "scipy") +@requires_pkg("rasterstats", "scipy", "shapely") def test_rasters(example_data_path): ws = example_data_path / "options" raster_name = "dem.img" diff --git a/autotest/test_mf6.py b/autotest/test_mf6.py index 3a35108f2..f9b475a4a 100644 --- a/autotest/test_mf6.py +++ b/autotest/test_mf6.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from modflow_devtools.markers import requires_exe +from modflow_devtools.markers import requires_exe, requires_pkg from modflow_devtools.misc import set_dir import flopy @@ -637,6 +637,7 @@ def test_binary_write(function_tmpdir, layered): @requires_exe("mf6") +@requires_pkg("shapely", "scipy") @pytest.mark.parametrize("layered", [True, False]) def test_vor_binary_write(function_tmpdir, layered): # build voronoi grid diff --git a/autotest/test_model_splitter.py b/autotest/test_model_splitter.py index 08cbb42c5..8e8dfbe24 100644 --- a/autotest/test_model_splitter.py +++ b/autotest/test_model_splitter.py @@ -200,6 +200,7 @@ def test_metis_splitting_with_lak_sfr(function_tmpdir): @requires_exe("mf6") +@requires_pkg("pymetis") def test_save_load_node_mapping(function_tmpdir): sim_path = get_example_data_path() / "mf6-freyberg" new_sim_path = function_tmpdir / "mf6-freyberg/split_model"