From 39b013810eafe30a9d491eccea4abcd70f39623b Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Mon, 8 Aug 2022 10:23:50 +1200 Subject: [PATCH] test: refine pytest skip framework, improve confest.py * add "pytest_report_header" to show packages installed (or not) * remove "requires_exes", adjust "requires_exe" * add "has_exe", "has_pkg", and "requires_pkg" functions * use functions for tests, with a combination of installed packages * add "run_cmd" and "run_py_script" functions * example notebooks and scripts can skip if they use missing optional dep * update DEVELOPER.md with new changes --- .github/workflows/commit.yml | 4 +- DEVELOPER.md | 27 +- autotest/conftest.py | 120 +++++++-- autotest/regression/test_lgr.py | 10 +- autotest/regression/test_mf6.py | 46 ++-- autotest/regression/test_mf6_examples.py | 4 +- autotest/regression/test_mfnwt.py | 5 +- autotest/regression/test_modflow.py | 21 +- autotest/regression/test_str.py | 7 +- autotest/regression/test_swi2.py | 4 +- autotest/regression/test_wel.py | 4 +- autotest/test_conftest.py | 17 +- autotest/test_example_notebooks.py | 18 +- autotest/test_example_scripts.py | 17 +- autotest/test_export.py | 97 +++---- autotest/test_geospatial_util.py | 12 + autotest/test_grid.py | 9 + autotest/test_gridgen.py | 20 +- autotest/test_gridintersect.py | 324 ++++++----------------- autotest/test_headufile.py | 9 +- autotest/test_hydmodfile.py | 8 +- autotest/test_lake_connections.py | 5 +- autotest/test_listbudget.py | 12 +- autotest/test_mnw.py | 6 +- autotest/test_modflow.py | 5 +- autotest/test_modpathfile.py | 10 +- autotest/test_mp6.py | 18 +- autotest/test_mp7.py | 28 +- autotest/test_mt3d.py | 28 +- autotest/test_obs.py | 11 +- autotest/test_plot.py | 3 + autotest/test_scripts.py | 14 +- autotest/test_sfr.py | 16 +- autotest/test_str.py | 8 +- autotest/test_swr_binaryread.py | 8 +- autotest/test_util_2d_and_3d.py | 5 + autotest/test_uzf.py | 6 +- autotest/test_zonbud_utility.py | 10 +- etc/environment.yml | 8 +- setup.cfg | 10 +- 40 files changed, 478 insertions(+), 516 deletions(-) diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index 1a8e8c98c2..9264d2ac10 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -150,7 +150,6 @@ jobs: ${{ runner.os }}-${{ hashFiles('flopy/utils/get_modflow.py') }} - name: Install Modflow executables - working-directory: ./autotest run: | mkdir -p $HOME/.local/bin get-modflow $HOME/.local/bin @@ -241,7 +240,6 @@ jobs: ${{ runner.os }}-${{ hashFiles('flopy/utils/get_modflow.py') }} - name: Install Modflow executables - working-directory: ./autotest run: | mkdir -p $HOME/.local/bin get-modflow $HOME/.local/bin @@ -359,4 +357,4 @@ jobs: uses: codecov/codecov-action@v2.1.0 with: directory: ./autotest - file: coverage.xml \ No newline at end of file + file: coverage.xml diff --git a/DEVELOPER.md b/DEVELOPER.md index d4d65cc8cc..92f7a5447c 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -342,9 +342,9 @@ def test_get_paths(): #### Conditionally skipping tests -Several `pytest` markers are provided to conditionally skip tests based on executable availability or operating system. +Several `pytest` markers are provided to conditionally skip tests based on executable availability, Python package environment or operating system. -To skip tests if an executable is not available on the path: +To skip tests if one or more executables are not available on the path: ```python from shutil import which @@ -353,19 +353,26 @@ from autotest.conftest import requires_exe @requires_exe("mf6") def test_mf6(): assert which("mf6") + +@requires_exe("mf6", "mp7") +def test_mf6_and_mp7(): + assert which("mf6") + assert which("mp7") ``` -A variant for multiple executables is also provided: +To skip tests if one or more Python packages are not available: ```python -from shutil import which -from autotest.conftest import requires_exes +from autotest.conftest import requires_pkg -exes = ["mfusg", "mfnwt"] +@requires_pkg("pandas") +def test_needs_pandas(): + import pandas as pd -@requires_exes(exes) -def test_mfusg_and_mfnwt(): - assert all(which(exe) for exe in exes) +@requires_pkg("pandas", "shapefile") +def test_needs_pandas(): + import pandas as pd + from shapefile import Reader ``` To mark tests requiring or incompatible with particular operating systems: @@ -430,4 +437,4 @@ act -W .github/workflows/ci.yml -j build The `-n` flag can be used to execute a dry run, which doesn't run anything, just evaluates workflow, job and step definitions. See the [docs](https://github.com/nektos/act#example-commands) for more. -**Note:** `act` can only run Linux-based container definitions, so Mac or Windows workflows or matrix OS entries will be skipped. \ No newline at end of file +**Note:** `act` can only run Linux-based container definitions, so Mac or Windows workflows or matrix OS entries will be skipped. diff --git a/autotest/conftest.py b/autotest/conftest.py index 6444307e24..93988d64ba 100644 --- a/autotest/conftest.py +++ b/autotest/conftest.py @@ -1,11 +1,13 @@ import os +import pkg_resources import socket -import subprocess +import sys from os import environ from os.path import basename, normpath from pathlib import Path from platform import system from shutil import copytree, which +from subprocess import PIPE, Popen from typing import List, Optional from urllib import request from warnings import warn @@ -114,23 +116,12 @@ def get_current_branch() -> str: return basename(normpath(ref)).lower() # otherwise ask git about it - try: - b = subprocess.Popen( - ("git", "status"), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ).communicate()[0] - - if isinstance(b, bytes): - b = b.decode("utf-8") - - for line in b.splitlines(): - if "On branch" in line: - return line.replace("On branch ", "").rstrip().lower() - except: - raise ValueError( - "Could not determine current branch. Is git installed?" - ) + if not which("git"): + raise RuntimeError("'git' required to determine current branch") + stdout, stderr, code = run_cmd("git", "rev-parse", "--abbrev-ref", "HEAD") + if code == 0 and stdout: + return stdout.strip().lower() + raise ValueError(f"Could not determine current branch: {stderr}") def is_connected(hostname): @@ -172,15 +163,39 @@ def is_github_rate_limited() -> Optional[bool]: return None -def requires_exes(exes): +_has_exe_cache = {} +_has_pkg_cache = {} + +def has_exe(exe): + if exe not in _has_exe_cache: + _has_exe_cache[exe] = bool(which(exe)) + return _has_exe_cache[exe] + +def has_pkg(pkg): + if pkg not in _has_pkg_cache: + try: + _has_pkg_cache[pkg] = bool(pkg_resources.get_distribution(pkg)) + except pkg_resources.DistributionNotFound: + _has_pkg_cache[pkg] = False + return _has_pkg_cache[pkg] + + +def requires_exe(*exes): + missing = {exe for exe in exes if not has_exe(exe)} return pytest.mark.skipif( - any(which(exe) is None for exe in exes), - reason=f"requires executables: {', '.join(exes)}", + missing, + reason=f"missing executable{'s' if len(missing) != 1 else ''}: " + + ", ".join(missing), ) -def requires_exe(exe): - return requires_exes([exe]) +def requires_pkg(*pkgs): + missing = {pkg for pkg in pkgs if not has_pkg(pkg)} + return pytest.mark.skipif( + missing, + reason=f"missing package{'s' if len(missing) != 1 else ''}: " + + ", ".join(missing), + ) def requires_platform(platform, ci_only=False): @@ -388,3 +403,62 @@ def pytest_runtest_setup(item): is_profiletest = any(item.iter_markers(name="profile")) if (is_profiletest and not should_profile) or (not is_profiletest and should_profile): pytest.skip() + + +def pytest_report_header(config): + """Header for pytest to show versions of packages.""" + processed = set() + flopy_pkg = pkg_resources.get_distribution("flopy") + lines = [] + items = [] + for pkg in flopy_pkg.requires(): + name = pkg.name + processed.add(name) + try: + version = pkg_resources.get_distribution(name).version + items.append(f"{name}-{version}") + except pkg_resources.DistributionNotFound: + items.append(f"{name} (not found)") + lines.append("required packages: " + ", ".join(items)) + installed = [] + not_found = [] + for pkg in flopy_pkg.requires(["optional"]): + name = pkg.name + if name in processed: + continue + processed.add(name) + try: + version = pkg_resources.get_distribution(name).version + installed.append(f"{name}-{version}") + except pkg_resources.DistributionNotFound: + not_found.append(name) + if installed: + lines.append("optional packages: " + ", ".join(installed)) + if not_found: + lines.append("optional packages not found: " + ", ".join(not_found)) + return "\n".join(lines) + + +# functions to run commands and scripts + +def run_cmd(*args, verbose=False, **kwargs): + """Run any command, return tuple (stdout, stderr, returncode).""" + args = [str(g) for g in args] + if verbose: + print("running: " + " ".join(args)) + p = Popen(args, stdout=PIPE, stderr=PIPE, **kwargs) + stdout, stderr = p.communicate() + stdout = stdout.decode() + stderr = stderr.decode() + returncode = p.returncode + if verbose: + print(f"stdout:\n{stdout}") + print(f"stderr:\n{stderr}") + print(f"returncode: {returncode}") + return stdout, stderr, returncode + + +def run_py_script(script, *args, verbose=False): + """Run a Python script, return tuple (stdout, stderr, returncode).""" + return run_cmd( + sys.executable, script, *args, verbose=verbose, cwd=Path(script).parent) diff --git a/autotest/regression/test_lgr.py b/autotest/regression/test_lgr.py index 7898d4ceee..040182ed86 100644 --- a/autotest/regression/test_lgr.py +++ b/autotest/regression/test_lgr.py @@ -5,18 +5,18 @@ import pytest import flopy -from autotest.conftest import requires_exe +from autotest.conftest import requires_exe, requires_pkg @requires_exe("mflgr") +@requires_pkg("pymake") @pytest.mark.regression def test_simplelgr(tmpdir, example_data_path): - mflgr_v2_ex3_path = example_data_path / "mflgr_v2" / "ex3" - - pytest.importorskip("pymake") + """Test load and write of distributed MODFLOW-LGR example problem.""" import pymake - # Test load and write of distributed MODFLOW-LGR example problem + mflgr_v2_ex3_path = example_data_path / "mflgr_v2" / "ex3" + ws = tmpdir / mflgr_v2_ex3_path.stem shutil.copytree(mflgr_v2_ex3_path, ws) diff --git a/autotest/regression/test_mf6.py b/autotest/regression/test_mf6.py index c1d54b90b3..19335bf185 100644 --- a/autotest/regression/test_mf6.py +++ b/autotest/regression/test_mf6.py @@ -8,7 +8,7 @@ import pytest import flopy -from autotest.conftest import requires_exe +from autotest.conftest import requires_exe, requires_pkg from flopy.mf6 import MFSimulation, ModflowTdis, ModflowGwfgwf, ModflowGwfgwt, ModflowIms, ModflowGwf, ModflowGwfdis, ModflowGwfic, \ ModflowGwfnpf, ModflowGwfoc, ModflowGwfsto, ModflowGwfsfr, ModflowGwfwel, ModflowGwfdrn, ModflowGwfriv, ModflowGwfhfb, ModflowGwfchd, \ ModflowGwfghb, ModflowGwfrcha, ModflowUtltas, ModflowGwfevt, ModflowGwfrch, ModflowGwfdisv, ModflowGwfgnc, ModflowGwfevta, MFModel, ModflowGwtdis, \ @@ -21,9 +21,9 @@ @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test_np001(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -589,9 +589,9 @@ def test_np001(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test_np002(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -866,9 +866,9 @@ def test_np002(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test021_twri(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -1092,10 +1092,10 @@ def test021_twri(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test005_create_tests_advgw_tidal(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -1720,9 +1720,9 @@ def test005_create_tests_advgw_tidal(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test004_create_tests_bcfss(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -1871,9 +1871,9 @@ def test004_create_tests_bcfss(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test035_create_tests_fhb(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -2014,10 +2014,9 @@ def test035_create_tests_fhb(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake", "shapefile") @pytest.mark.regression def test006_create_tests_gwf3_disv(tmpdir, example_data_path): - pytest.importorskip("shapefile") - pytest.importorskip("pymake") import pymake # init paths @@ -2309,9 +2308,9 @@ def test006_create_tests_gwf3_disv(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test006_create_tests_2models_gnc(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -2649,10 +2648,10 @@ def test006_create_tests_2models_gnc(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test050_create_tests_circle_island(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -2750,11 +2749,11 @@ def test050_create_tests_circle_island(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.xfail(reason="possible python3.7/windows incompatibilities in testutils.read_std_array " "https://github.com/modflowpy/flopy/runs/7581629193?check_suite_focus=true#step:11:1753") @pytest.mark.regression def test028_create_tests_sfr(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -3023,9 +3022,9 @@ def test028_create_tests_sfr(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test_create_tests_transport(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -3263,11 +3262,10 @@ def test_create_tests_transport(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake", "shapely") @pytest.mark.slow @pytest.mark.regression def test001a_tharmonic(tmpdir, example_data_path): - pytest.importorskip("shapely") - pytest.importorskip("pymake") import pymake # init paths @@ -3396,9 +3394,9 @@ def test001a_tharmonic(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test003_gwfs_disv(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -3489,10 +3487,10 @@ def test003_gwfs_disv(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test005_advgw_tidal(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -3557,9 +3555,9 @@ def test005_advgw_tidal(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test006_gwf3(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -3742,9 +3740,9 @@ def test006_gwf3(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test045_lake1ss_table(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -3821,10 +3819,10 @@ def test045_lake1ss_table(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test006_2models_mvr(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -4010,10 +4008,10 @@ def test006_2models_mvr(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test001e_uzf_3lay(tmpdir, example_data_path): - pytest.importorskip("pymake") # init paths test_ex_name = "test001e_UZF_3lay" @@ -4110,10 +4108,10 @@ def test001e_uzf_3lay(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test045_lake2tr(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -4185,9 +4183,9 @@ def test045_lake2tr(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.regression def test036_twrihfb(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths @@ -4269,10 +4267,10 @@ def test036_twrihfb(tmpdir, example_data_path): @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test027_timeseriestest(tmpdir, example_data_path): - pytest.importorskip("pymake") import pymake # init paths diff --git a/autotest/regression/test_mf6_examples.py b/autotest/regression/test_mf6_examples.py index cd362f07ee..ed0ad9a9c3 100644 --- a/autotest/regression/test_mf6_examples.py +++ b/autotest/regression/test_mf6_examples.py @@ -3,12 +3,13 @@ import pytest -from autotest.conftest import requires_exe +from autotest.conftest import requires_exe, requires_pkg from autotest.regression.conftest import is_nested from flopy.mf6 import MFSimulation @requires_exe("mf6") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test_mf6_example_simulations(tmpdir, mf6_example_namfiles): @@ -22,7 +23,6 @@ def test_mf6_example_simulations(tmpdir, mf6_example_namfiles): mf6_example_namfiles: ordered list of namfiles for 1+ coupled models """ - pytest.importorskip("pymake") import pymake # make sure we have at least 1 name file diff --git a/autotest/regression/test_mfnwt.py b/autotest/regression/test_mfnwt.py index 373d534bf9..fb16da80f4 100644 --- a/autotest/regression/test_mfnwt.py +++ b/autotest/regression/test_mfnwt.py @@ -2,7 +2,7 @@ import pytest -from autotest.conftest import get_example_data_path +from autotest.conftest import get_example_data_path, requires_exe, requires_pkg from flopy.modflow import Modflow, ModflowUpw, ModflowNwt from flopy.utils import parsenamefile @@ -26,11 +26,12 @@ def get_nfnwt_namfiles(): return namfiles +@requires_exe("mfnwt") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression @pytest.mark.parametrize("namfile", get_nfnwt_namfiles()) def test_run_mfnwt_model(tmpdir, namfile): - pytest.importorskip("pymake") import pymake # load a MODFLOW-2005 model, convert to a MFNWT model, diff --git a/autotest/regression/test_modflow.py b/autotest/regression/test_modflow.py index cf7d8dba71..d02e928f8f 100644 --- a/autotest/regression/test_modflow.py +++ b/autotest/regression/test_modflow.py @@ -5,7 +5,7 @@ import pytest -from autotest.conftest import requires_exe, get_example_data_path +from autotest.conftest import requires_exe, requires_pkg, get_example_data_path from flopy.modflow import Modflow, ModflowOc @@ -20,10 +20,10 @@ def uzf_example_path(example_data_path): @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test_uzf_unit_numbers(tmpdir, uzf_example_path): - pytest.importorskip("pymake") import pymake mfnam = "UZFtest2.nam" @@ -84,10 +84,10 @@ def test_uzf_unit_numbers(tmpdir, uzf_example_path): @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test_unitnums(tmpdir, mf2005_test_path): - pytest.importorskip("pymake") import pymake mfnam = "testsfr2_tab.nam" @@ -125,15 +125,14 @@ def test_unitnums(tmpdir, mf2005_test_path): @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression def test_gage(tmpdir, example_data_path): - pytest.importorskip("pymake") - import pymake - """ test043 load and write of MODFLOW-2005 GAGE example problem """ + import pymake pth = str(example_data_path / "mf2005_test") fpth = join(pth, "testsfr2_tab.nam") @@ -174,6 +173,7 @@ def test_gage(tmpdir, example_data_path): @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression @pytest.mark.parametrize("namfile", [ @@ -181,7 +181,6 @@ def test_gage(tmpdir, example_data_path): for nf in ["twri.nam", "MNW2.nam"] ]) def test_mf2005pcgn(tmpdir, namfile): - pytest.importorskip("pymake") import pymake ws = tmpdir / "ws" @@ -225,11 +224,11 @@ def test_mf2005pcgn(tmpdir, namfile): @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression @pytest.mark.parametrize("namfile", [str(__example_data_path / "secp" / nf) for nf in ["secp.nam"]]) def test_mf2005gmg(tmpdir, namfile): - pytest.importorskip("pymake") import pymake ws = tmpdir / "ws" @@ -268,13 +267,13 @@ def test_mf2005gmg(tmpdir, namfile): @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.regression @pytest.mark.parametrize("namfile", [str(__example_data_path / "freyberg" / nf) for nf in ["freyberg.nam"]]) def test_mf2005(tmpdir, namfile): """ test045 load and write of MODFLOW-2005 GMG example problem """ - pytest.importorskip("pymake") import pymake compth = tmpdir / "flopy" @@ -345,11 +344,11 @@ def test_mf2005(tmpdir, namfile): @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression @pytest.mark.parametrize("namfile", mf2005_namfiles) def test_mf2005fhb(tmpdir, namfile): - pytest.importorskip("pymake") import pymake ws = str(tmpdir / "ws") @@ -382,11 +381,11 @@ def test_mf2005fhb(tmpdir, namfile): @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression @pytest.mark.parametrize("namfile", mf2005_namfiles) def test_mf2005_lake(tmpdir, namfile, mf2005_test_path): - pytest.importorskip("pymake") import pymake ws = str(tmpdir / "ws") diff --git a/autotest/regression/test_str.py b/autotest/regression/test_str.py index 6e812695c0..a98816d2e3 100644 --- a/autotest/regression/test_str.py +++ b/autotest/regression/test_str.py @@ -1,6 +1,6 @@ import pytest -from autotest.conftest import requires_exe +from autotest.conftest import requires_exe, requires_pkg from flopy.modflow import Modflow, ModflowStr, ModflowOc str_items = { @@ -13,12 +13,13 @@ @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.regression def test_str_fixed_free(tmpdir, example_data_path): - mf2005_model_path = example_data_path / "mf2005_test" - pytest.importorskip("pymake") import pymake + mf2005_model_path = example_data_path / "mf2005_test" + m = Modflow.load( str_items[0]["mfnam"], exe_name="mf2005", diff --git a/autotest/regression/test_swi2.py b/autotest/regression/test_swi2.py index 66275e986f..a8e78c5f1a 100644 --- a/autotest/regression/test_swi2.py +++ b/autotest/regression/test_swi2.py @@ -3,7 +3,7 @@ import pytest -from autotest.conftest import requires_exe +from autotest.conftest import requires_exe, requires_pkg from flopy.modflow import Modflow @@ -13,11 +13,11 @@ def swi_path(example_data_path): @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression @pytest.mark.parametrize("namfile", ["swiex1.nam", "swiex2_strat.nam", "swiex3.nam"]) def test_mf2005swi2(tmpdir, swi_path, namfile): - pytest.importorskip("pymake") import pymake name = namfile.replace(".nam", "") diff --git a/autotest/regression/test_wel.py b/autotest/regression/test_wel.py index eb47f075f6..7f30f02ef4 100644 --- a/autotest/regression/test_wel.py +++ b/autotest/regression/test_wel.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from autotest.conftest import requires_exe +from autotest.conftest import requires_exe, requires_pkg from flopy.modflow import ( Modflow, ModflowBas, @@ -16,9 +16,9 @@ @requires_exe("mf2005") +@requires_pkg("pymake") @pytest.mark.regression def test_binary_well(tmpdir): - pytest.importorskip("pymake") import pymake nlay = 3 diff --git a/autotest/test_conftest.py b/autotest/test_conftest.py index f7ca4342d6..b13b6ac2b9 100644 --- a/autotest/test_conftest.py +++ b/autotest/test_conftest.py @@ -7,7 +7,7 @@ import pytest from _pytest.config import ExitCode -from autotest.conftest import get_project_root_path, get_example_data_path, requires_exe, requires_exes, requires_platform, excludes_platform +from autotest.conftest import get_project_root_path, get_example_data_path, requires_exe, requires_pkg, requires_platform, excludes_platform # temporary directory fixtures @@ -128,11 +128,24 @@ def test_mf6(): exes = ["mfusg", "mfnwt"] -@requires_exes(exes) +@requires_exe(*exes) def test_mfusg_and_mfnwt(): assert all(which(exe) for exe in exes) +@requires_pkg("numpy") +def test_numpy(): + import numpy + assert numpy is not None + + +@requires_pkg("numpy", "matplotlib") +def test_numpy_and_matplotlib(): + import numpy + import matplotlib + assert numpy is not None and matplotlib is not None + + @requires_platform("Windows") def test_needs_windows(): assert platform.system() == "Windows" diff --git a/autotest/test_example_notebooks.py b/autotest/test_example_notebooks.py index d253e809ca..56aa2633d7 100644 --- a/autotest/test_example_notebooks.py +++ b/autotest/test_example_notebooks.py @@ -1,8 +1,8 @@ -import os +import re import pytest -from autotest.conftest import get_project_root_path +from autotest.conftest import get_project_root_path, run_cmd def get_example_notebooks(exclude=None): @@ -17,5 +17,15 @@ def get_example_notebooks(exclude=None): @pytest.mark.example @pytest.mark.parametrize("notebook", get_example_notebooks(exclude=["mf6_lgr"])) # TODO: figure out why this one fails def test_notebooks(notebook): - arg = ("jupytext", "--from ipynb", "--execute", notebook) - assert os.system(" ".join(arg)) == 0, f"could not run {notebook}" + args = ["jupytext", "--from", "ipynb", "--execute", notebook] + stdout, stderr, returncode = run_cmd(*args, verbose=True) + + if returncode != 0: + if "Missing optional dependency" in stderr: + pkg = re.findall("Missing optional dependency '(.*)'", stderr)[0] + pytest.skip(f"notebook requires optional dependency {pkg!r}") + elif "No module named " in stderr: + pkg = re.findall("No module named '(.*)'", stderr)[0] + pytest.skip(f"notebook requires package {pkg!r}") + + assert returncode == 0, f"could not run {notebook}" diff --git a/autotest/test_example_scripts.py b/autotest/test_example_scripts.py index 8980638fa2..8f31ebb783 100644 --- a/autotest/test_example_scripts.py +++ b/autotest/test_example_scripts.py @@ -1,11 +1,11 @@ +import re from functools import reduce from os import linesep from pathlib import Path -from subprocess import PIPE, Popen import pytest -from autotest.conftest import get_project_root_path +from autotest.conftest import get_project_root_path, run_py_script def get_example_scripts(exclude=None): @@ -23,9 +23,14 @@ def get_example_scripts(exclude=None): @pytest.mark.example @pytest.mark.parametrize("script", get_example_scripts()) def test_scripts(script): - proc = Popen(("python", Path(script).name), stdout=PIPE, stderr=PIPE, cwd=Path(script).parent) - stdout, stderr = proc.communicate() - if stdout: print(stdout.decode("utf-8")) + stdout, stderr, returncode = run_py_script(script, verbose=True) + + if returncode != 0: + if "Missing optional dependency" in stderr: + pkg = re.findall("Missing optional dependency '(.*)'", stderr)[0] + pytest.skip(f"script requires optional dependency {pkg!r}") + + assert returncode == 0 allowed_patterns = [ "findfont", @@ -35,4 +40,4 @@ def test_scripts(script): assert (not stderr or # trap warnings & non-fatal errors - all((not line or any(p in line.lower() for p in allowed_patterns)) for line in stderr.decode("utf-8").split(linesep))) + all((not line or any(p in line.lower() for p in allowed_patterns)) for line in stderr.split(linesep))) diff --git a/autotest/test_export.py b/autotest/test_export.py index 8bcb1cdc8d..3322b48f9f 100644 --- a/autotest/test_export.py +++ b/autotest/test_export.py @@ -1,7 +1,6 @@ import os import shutil from pathlib import Path -from shutil import which from typing import List import math @@ -13,6 +12,9 @@ from autotest.conftest import ( SHAPEFILE_EXTENSIONS, get_example_data_path, + has_pkg, + requires_exe, + requires_pkg, ) from flopy.discretization import StructuredGrid, UnstructuredGrid from flopy.export import NetCdf @@ -60,8 +62,8 @@ def namfiles(): return [str(p) for p in Path(mf2005_path).rglob("*.nam")] +@requires_pkg("shapefile") def test_output_helper_shapefile_export(tmpdir, example_data_path): - pytest.importorskip("shapefile") ml = Modflow.load( "freyberg.nam", @@ -79,9 +81,9 @@ def test_output_helper_shapefile_export(tmpdir, example_data_path): ) +@requires_pkg("pandas", "shapefile") @pytest.mark.slow def test_freyberg_export(tmpdir, example_data_path): - pytest.importorskip("shapefile") # steady state namfile = "freyberg.nam" @@ -169,9 +171,8 @@ def test_freyberg_export(tmpdir, example_data_path): assert prjtxt == wkt +@requires_pkg("netCDF4", "pyproj") def test_export_output(tmpdir, example_data_path): - pytest.importorskip("netCDF4") - pytest.importorskip("pyproj") ml = Modflow.load( "freyberg.nam", model_ws=str(example_data_path / "freyberg") @@ -193,8 +194,8 @@ def test_export_output(tmpdir, example_data_path): nc.nc.close() +@requires_pkg("shapefile", "shapely") def test_write_grid_shapefile(tmpdir): - pytest.importorskip("shapefile") from shapefile import Reader from flopy.discretization import StructuredGrid @@ -239,8 +240,8 @@ def test_write_grid_shapefile(tmpdir): pass +@requires_pkg("shapefile") def test_export_shapefile_polygon_closed(tmpdir): - pytest.importorskip("shapefile") from shapefile import Reader xll, yll = 468970, 3478635 @@ -271,10 +272,10 @@ def test_export_shapefile_polygon_closed(tmpdir): shp.close() +@requires_pkg("rasterio", "shapefile", "scipy") def test_export_array(tmpdir, example_data_path): - pytest.importorskip("shapefile") - pytest.importorskip("scipy") from scipy.ndimage import rotate + import rasterio namfile = "freyberg.nam" model_ws = example_data_path / "freyberg" @@ -321,9 +322,6 @@ def test_export_array(tmpdir, example_data_path): rotated = rotate(m.dis.top.array, m.modelgrid.angrot, cval=nodata) assert rotated.shape == arr.shape - pytest.importorskip("rasterio") - import rasterio - export_array( m.modelgrid, os.path.join(tmpdir, "fb.tif"), @@ -339,10 +337,8 @@ def test_export_array(tmpdir, example_data_path): pass +@requires_pkg("netCDF4", "pyproj") def test_netcdf_classmethods(tmpdir, example_data_path): - pytest.importorskip("netCDF4") - pytest.importorskip("pyproj") - namfile = "freyberg.nam" name = namfile.replace(".nam", "") model_ws = example_data_path / "freyberg_multilayer_transient" @@ -402,8 +398,8 @@ def test_wkt_parse(example_shapefiles): assert crsobj.__dict__[k] is not None +@requires_pkg("shapefile") def test_shapefile_ibound(tmpdir, example_data_path): - pytest.importorskip("shapefile") from shapefile import Reader shape_name = os.path.join(tmpdir, "test.shp") @@ -425,10 +421,10 @@ def test_shapefile_ibound(tmpdir, example_data_path): shape.close() +@requires_pkg("pandas", "shapefile") @pytest.mark.slow @pytest.mark.parametrize("namfile", namfiles()) def test_shapefile(tmpdir, namfile): - pytest.importorskip("shapefile") from shapefile import Reader model = flopy.modflow.Modflow.load( @@ -450,10 +446,10 @@ def test_shapefile(tmpdir, namfile): ), f"wrong number of records in shapefile {fnc_name}" +@requires_pkg("pandas", "shapefile") @pytest.mark.slow @pytest.mark.parametrize("namfile", namfiles()) def test_shapefile_export_modelgrid_override(tmpdir, namfile): - pytest.importorskip("shapefile") from shapefile import Reader model = flopy.modflow.Modflow.load( @@ -489,11 +485,10 @@ def test_shapefile_export_modelgrid_override(tmpdir, namfile): s.close() +@requires_pkg("netCDF4", "pyproj") @pytest.mark.slow @pytest.mark.parametrize("namfile", namfiles()) def test_export_netcdf(tmpdir, namfile): - pytest.importorskip("pyproj") - pytest.importorskip("netCDF4") from netCDF4 import Dataset model = flopy.modflow.Modflow.load( @@ -519,9 +514,8 @@ def test_export_netcdf(tmpdir, namfile): nc.close() +@requires_pkg("shapefile") def test_export_array2(tmpdir): - pytest.importorskip("shapefile") - nrow = 7 ncol = 11 epsg = 4111 @@ -554,9 +548,8 @@ def test_export_array2(tmpdir): assert os.path.isfile(filename), "did not create array shapefile" +@requires_pkg("shapefile", "shapely") def test_export_array_contours(tmpdir): - pytest.importorskip("shapefile") - nrow = 7 ncol = 11 epsg = 4111 @@ -589,8 +582,8 @@ def test_export_array_contours(tmpdir): assert os.path.isfile(filename), "did not create contour shapefile" +@requires_pkg("shapefile", "shapely") def test_export_contourf(tmpdir, example_data_path): - pytest.importorskip("shapefile") from shapefile import Reader filename = os.path.join(tmpdir, "myfilledcontours.shp") @@ -619,6 +612,7 @@ def test_export_contourf(tmpdir, example_data_path): ) +@requires_pkg("shapely") def test_mf6_grid_shp_export(tmpdir): nlay = 2 nrow = 10 @@ -717,7 +711,8 @@ def cellid(k, i, j, nrow, ncol): ), f"variable {k} is not equal" pass - pytest.importorskip("shapefile") + if not has_pkg("shapefile"): + return # rch6.export('{}/mf6.shp'.format(baseDir)) m.export(str(tmpdir / "mfnwt.shp")) @@ -749,10 +744,9 @@ def cellid(k, i, j, nrow, ncol): assert np.abs(it - it6) < 1e-6 +@requires_pkg("shapefile") @pytest.mark.slow def test_export_huge_shapefile(tmpdir): - pytest.importorskip("shapefile") - nlay = 2 nrow = 200 ncol = 200 @@ -781,6 +775,7 @@ def test_export_huge_shapefile(tmpdir): m.export(str(tmpdir / "huge.shp")) +@requires_pkg("pyproj") def test_polygon_from_ij(tmpdir): """test creation of a polygon from an i, j location using get_vertices().""" ws = str(tmpdir) @@ -877,9 +872,8 @@ def count_lines_in_file(filepath, binary=False): return n +@requires_pkg("vtk") def test_vtk_export_array2d(tmpdir, example_data_path): - pytest.importorskip("vtk") - # test mf 2005 freyberg mpath = str(example_data_path / "freyberg_multilayer_transient") namfile = "freyberg.nam" @@ -902,9 +896,8 @@ def test_vtk_export_array2d(tmpdir, example_data_path): assert nlines1 == 17615 +@requires_pkg("vtk") def test_vtk_export_array3d(tmpdir, example_data_path): - pytest.importorskip("vtk") - # test mf 2005 freyberg mpath = str(example_data_path / "freyberg_multilayer_transient") namfile = "freyberg.nam" @@ -944,9 +937,8 @@ def test_vtk_export_array3d(tmpdir, example_data_path): assert os.path.exists(filetocheck) +@requires_pkg("vtk") def test_vtk_transient_array_2d(tmpdir, example_data_path): - pytest.importorskip("vtk") - # test mf 2005 freyberg ws = str(tmpdir) mpath = str(example_data_path / "freyberg_multilayer_transient") @@ -978,10 +970,9 @@ def test_vtk_transient_array_2d(tmpdir, example_data_path): assert os.path.exists(filetocheck) +@requires_pkg("vtk") @pytest.mark.slow def test_vtk_export_packages(tmpdir, example_data_path): - pytest.importorskip("vtk") - # test mf 2005 freyberg ws = str(tmpdir) mpath = str(example_data_path / "freyberg_multilayer_transient") @@ -1035,9 +1026,8 @@ def test_vtk_export_packages(tmpdir, example_data_path): assert os.path.exists(filetocheck) +@requires_pkg("vtk") def test_vtk_mf6(tmpdir, example_data_path): - pytest.importorskip("vtk") - # test mf6 mf6expth = str(example_data_path / "mf6") mf6sims = [ @@ -1066,10 +1056,9 @@ def test_vtk_mf6(tmpdir, example_data_path): assert nlines == 9537 +@requires_pkg("vtk") @pytest.mark.slow def test_vtk_binary_head_export(tmpdir, example_data_path): - pytest.importorskip("vtk") - # test mf 2005 freyberg ws = str(tmpdir) mpth = str(example_data_path / "freyberg_multilayer_transient") @@ -1118,10 +1107,9 @@ def test_vtk_binary_head_export(tmpdir, example_data_path): assert nlines2 == 34 +@requires_pkg("vtk") @pytest.mark.slow def test_vtk_cbc(tmpdir, example_data_path): - pytest.importorskip("vtk") - # test mf 2005 freyberg ws = str(tmpdir) @@ -1151,10 +1139,9 @@ def test_vtk_cbc(tmpdir, example_data_path): assert os.path.exists(filetocheck) +@requires_pkg("vtk") @pytest.mark.slow def test_vtk_vector(tmpdir, example_data_path): - pytest.importorskip("vtk") - # test mf 2005 freyberg mpth = str(example_data_path / "freyberg_multilayer_transient") namfile = "freyberg.nam" @@ -1213,8 +1200,8 @@ def test_vtk_vector(tmpdir, example_data_path): ), f"file (1) does not exist: {filetocheck}" +@requires_pkg("vtk") def test_vtk_unstructured(tmpdir, example_data_path): - pytest.importorskip("vtk") from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkIOLegacy import vtkUnstructuredGridReader @@ -1301,8 +1288,8 @@ def load_iverts(fname): assert np.allclose(np.ravel(top), top2), "Field data not properly written" +@requires_pkg("vtk") def test_vtk_vertex(tmpdir, example_data_path): - pytest.importorskip("vtk") from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkIOLegacy import vtkUnstructuredGridReader @@ -1336,11 +1323,9 @@ def test_vtk_vertex(tmpdir, example_data_path): ), "Field data not properly written" -@pytest.mark.skipif( - which("mf2005") is None, reason="requires mf2005 executable" -) +@requires_exe("mf2005") +@requires_pkg("vtk") def test_vtk_pathline(tmpdir, example_data_path): - pytest.importorskip("vtk") from vtkmodules.vtkIOLegacy import vtkUnstructuredGridReader # pathline test for vtk @@ -1461,8 +1446,8 @@ def load_iverts(fname, closed=False): return iverts, np.array(xc), np.array(yc) +@requires_pkg("vtk") def test_vtk_export_model_without_packages_names(tmpdir): - pytest.importorskip("vtk") from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkIOLegacy import vtkUnstructuredGridReader @@ -1518,8 +1503,8 @@ def test_vtk_export_model_without_packages_names(tmpdir): assert np.allclose(cell_types, cell_types_answer), errmsg +@requires_pkg("vtk") def test_vtk_export_disv1_model(tmpdir): - pytest.importorskip("vtk") from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkIOLegacy import vtkUnstructuredGridReader @@ -1590,8 +1575,8 @@ def test_vtk_export_disv1_model(tmpdir): assert np.allclose(cell_types, cell_types_answer), errmsg +@requires_pkg("vtk") def test_vtk_export_disv2_model(tmpdir): - pytest.importorskip("vtk") from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkIOLegacy import vtkUnstructuredGridReader @@ -1655,8 +1640,8 @@ def test_vtk_export_disv2_model(tmpdir): assert np.allclose(cell_types, cell_types_answer), errmsg +@requires_pkg("vtk") def test_vtk_export_disu1_grid(tmpdir, example_data_path): - pytest.importorskip("vtk") from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkIOLegacy import vtkUnstructuredGridReader @@ -1741,8 +1726,8 @@ def test_vtk_export_disu1_grid(tmpdir, example_data_path): assert np.allclose(cell_types, cell_types_answer), errmsg +@requires_pkg("vtk") def test_vtk_export_disu2_grid(tmpdir, example_data_path): - pytest.importorskip("vtk") from vtkmodules.util.numpy_support import vtk_to_numpy from vtkmodules.vtkIOLegacy import vtkUnstructuredGridReader @@ -1827,8 +1812,8 @@ def test_vtk_export_disu2_grid(tmpdir, example_data_path): assert np.allclose(cell_types, cell_types_answer), errmsg +@requires_pkg("vtk", "shapefile") def test_vtk_export_disu_model(tmpdir): - pytest.importorskip("vtk") from vtkmodules.util.numpy_support import vtk_to_numpy from flopy.export.vtk import Vtk diff --git a/autotest/test_geospatial_util.py b/autotest/test_geospatial_util.py index 98f7c70a83..309b31b25e 100644 --- a/autotest/test_geospatial_util.py +++ b/autotest/test_geospatial_util.py @@ -1,4 +1,5 @@ import pytest +from autotest.conftest import requires_pkg from flopy.utils.geometry import ( Collection, @@ -152,6 +153,7 @@ def test_import_geospatial_utils(): ) +@requires_pkg("shapely", "geojson") def test_polygon(polygon): poly = Shape.from_geojson(polygon) gi1 = poly.__geo_interface__ @@ -185,6 +187,7 @@ def test_polygon(polygon): raise AssertionError("GeoSpatialUtil polygon conversion error") +@requires_pkg("shapely", "geojson") def test_polygon_with_hole(poly_w_hole): from flopy.utils.geometry import Polygon, Shape from flopy.utils.geospatial_utils import GeoSpatialUtil @@ -222,6 +225,7 @@ def test_polygon_with_hole(poly_w_hole): raise AssertionError("GeoSpatialUtil polygon conversion error") +@requires_pkg("shapely", "geojson") def test_multipolygon(multipolygon): poly = Shape.from_geojson(multipolygon) gi1 = poly.__geo_interface__ @@ -258,6 +262,7 @@ def test_multipolygon(multipolygon): ) +@requires_pkg("shapely", "geojson") def test_point(point): pt = Shape.from_geojson(point) gi1 = pt.__geo_interface__ @@ -287,6 +292,7 @@ def test_point(point): raise AssertionError("GeoSpatialUtil point conversion error") +@requires_pkg("shapely", "geojson") def test_multipoint(multipoint): mpt = Shape.from_geojson(multipoint) gi1 = mpt.__geo_interface__ @@ -316,6 +322,7 @@ def test_multipoint(multipoint): raise AssertionError("GeoSpatialUtil multipoint conversion error") +@requires_pkg("shapely", "geojson") def test_linestring(linestring): lstr = Shape.from_geojson(linestring) gi1 = lstr.__geo_interface__ @@ -345,6 +352,7 @@ def test_linestring(linestring): raise AssertionError("GeoSpatialUtil linestring conversion error") +@requires_pkg("shapely", "geojson") def test_multilinestring(multilinestring): mlstr = Shape.from_geojson(multilinestring) gi1 = mlstr.__geo_interface__ @@ -376,6 +384,7 @@ def test_multilinestring(multilinestring): ) +@requires_pkg("shapely", "geojson") def test_polygon_collection(polygon, poly_w_hole, multipolygon): col = [ Shape.from_geojson(polygon), @@ -417,6 +426,7 @@ def test_polygon_collection(polygon, poly_w_hole, multipolygon): ) +@requires_pkg("shapely", "geojson") def test_point_collection(point, multipoint): col = [Shape.from_geojson(point), Shape.from_geojson(multipoint)] @@ -448,6 +458,7 @@ def test_point_collection(point, multipoint): ) +@requires_pkg("shapely", "geojson") def test_linestring_collection(linestring, multilinestring): col = [Shape.from_geojson(linestring), Shape.from_geojson(multilinestring)] @@ -479,6 +490,7 @@ def test_linestring_collection(linestring, multilinestring): ) +@requires_pkg("shapely", "geojson") def test_mixed_collection( polygon, poly_w_hole, diff --git a/autotest/test_grid.py b/autotest/test_grid.py index 384ff5962b..1fe600fa3d 100644 --- a/autotest/test_grid.py +++ b/autotest/test_grid.py @@ -6,6 +6,8 @@ from flaky import flaky from matplotlib import pyplot as plt +from autotest.conftest import requires_pkg + from flopy.discretization import StructuredGrid, UnstructuredGrid, VertexGrid from flopy.mf6 import MFSimulation, ModflowGwf, ModflowGwfdis, ModflowGwfdisv from flopy.modflow import Modflow, ModflowDis @@ -689,6 +691,7 @@ def test_unstructured_complete_grid(): assert np.allclose(zv, np.array([[1, 0], [0, -1]])) +@requires_pkg("shapely") def test_loading_argus_meshes(example_data_path): datapth = str(example_data_path / "unstructured") fnames = [fname for fname in os.listdir(datapth) if fname.endswith(".exp")] @@ -741,6 +744,7 @@ def load_iverts(fname): assert g.nnodes == g.ncpl.sum() == 1090 +@requires_pkg("shapely") def test_triangle_unstructured_grid(tmpdir): maximum_area = 30000.0 extent = (214270.0, 221720.0, 4366610.0, 4373510.0) @@ -772,6 +776,7 @@ def test_triangle_unstructured_grid(tmpdir): assert g.nnodes == g.ncpl == 2730 +@requires_pkg("shapely", "scipy") def test_voronoi_vertex_grid(tmpdir): xmin = 0.0 xmax = 2.0 @@ -868,6 +873,7 @@ def voronoi_grid_2(): return __voronoi_grid_2() +@requires_pkg("shapely", "scipy") @flaky @pytest.mark.parametrize( "grid_info", [__voronoi_grid_0(), __voronoi_grid_1(), __voronoi_grid_2()] @@ -901,6 +907,7 @@ def test_voronoi_grid(tmpdir, grid_info): assert len(ninvalid_cells) == 0, errmsg +@requires_pkg("shapely", "scipy") @flaky def test_voronoi_grid3(tmpdir): name = "vor3" @@ -950,6 +957,7 @@ def test_voronoi_grid3(tmpdir): assert len(ninvalid_cells) == 0, errmsg +@requires_pkg("shapely", "scipy") @flaky def test_voronoi_grid4(tmpdir): name = "vor4" @@ -990,6 +998,7 @@ def test_voronoi_grid4(tmpdir): assert len(ninvalid_cells) == 0, errmsg +@requires_pkg("shapely", "scipy") @flaky def test_voronoi_grid5(tmpdir): name = "vor5" diff --git a/autotest/test_gridgen.py b/autotest/test_gridgen.py index 7b09bc4a0a..9dd1a9273a 100644 --- a/autotest/test_gridgen.py +++ b/autotest/test_gridgen.py @@ -8,14 +8,14 @@ from matplotlib.collections import LineCollection, PathCollection, QuadMesh import flopy -from autotest.conftest import requires_exes +from autotest.conftest import has_pkg, requires_exe, requires_pkg from flopy.utils.gridgen import Gridgen @pytest.mark.slow -@requires_exes(["mf6", "gridgen"]) +@requires_exe("mf6", "gridgen") +@requires_pkg("shapely") def test_mf6disv(tmpdir): - pytest.importorskip("shapely") from shapely.geometry import Polygon name = "dummy" @@ -149,9 +149,9 @@ def test_mf6disv(tmpdir): @pytest.mark.slow -@requires_exes(["mf6", "gridgen"]) +@requires_exe("mf6", "gridgen") +@requires_pkg("shapely", "shapefile") def test_mf6disu(tmpdir): - pytest.importorskip("shapely") from shapely.geometry import Polygon name = "dummy" @@ -317,9 +317,9 @@ def test_mf6disu(tmpdir): @pytest.mark.slow -@requires_exes(["mfusg", "gridgen"]) +@requires_exe("mfusg", "gridgen") +@requires_pkg("shapely", "shapefile") def test_mfusg(tmpdir): - pytest.importorskip("shapely") from shapely.geometry import Polygon name = "dummy" @@ -456,7 +456,8 @@ def test_mfusg(tmpdir): @pytest.mark.slow -@requires_exes(["mfusg", "gridgen"]) +@requires_exe("mfusg", "gridgen") +@requires_pkg("shapely") def test_gridgen(tmpdir): # define the base grid and then create a couple levels of nested # refinement @@ -523,7 +524,8 @@ def test_gridgen(tmpdir): ) # skip remainder if pyshp is not installed - pytest.importorskip("shapefile") + if not has_pkg("shapefile"): + return rf0shp = os.path.join(ws, "rf0") xmin = 7 * delr diff --git a/autotest/test_gridintersect.py b/autotest/test_gridintersect.py index 6c53e57dc4..078353db48 100644 --- a/autotest/test_gridintersect.py +++ b/autotest/test_gridintersect.py @@ -3,7 +3,8 @@ import matplotlib.pyplot as plt import numpy as np import pytest -from descartes import PolygonPatch + +from autotest.conftest import has_pkg, requires_pkg import flopy.discretization as fgrid import flopy.plot as fplot @@ -12,6 +13,11 @@ from flopy.utils.gridintersect import GridIntersect from flopy.utils.triangle import Triangle +if has_pkg("shapely"): + from shapely.geometry import ( + Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon + ) + rtree_toggle = pytest.mark.parametrize("rtree", [True, False]) @@ -123,6 +129,8 @@ def plot_vertex_grid(tgr): def plot_ix_polygon_result(rec, ax): + from descartes import PolygonPatch + for i, ishp in enumerate(rec.ixshapes): ppi = PolygonPatch(ishp, facecolor=f"C{i % 10}") ax.add_patch(ppi) @@ -146,10 +154,8 @@ def plot_ix_point_result(rec, ax): # %% test point structured +@requires_pkg("shapely") def test_rect_grid_3d_point_outside(): - pytest.importorskip("shapely") - from shapely.geometry import Point - botm = np.concatenate([np.ones(4), np.zeros(4)]).reshape(2, 2, 2) gr = get_rect_grid(top=np.ones(4), botm=botm) ix = GridIntersect(gr, method="structured") @@ -157,10 +163,8 @@ def test_rect_grid_3d_point_outside(): assert len(result) == 0 +@requires_pkg("shapely") def test_rect_grid_3d_point_inside(): - pytest.importorskip("shapely") - from shapely.geometry import Point - botm = np.concatenate([np.ones(4), 0.5 * np.ones(4), np.zeros(4)]).reshape( 3, 2, 2 ) @@ -170,10 +174,8 @@ def test_rect_grid_3d_point_inside(): assert result.cellids[0] == (1, 1, 0) +@requires_pkg("shapely") def test_rect_grid_3d_point_above(): - pytest.importorskip("shapely") - from shapely.geometry import Point - botm = np.concatenate([np.ones(4), np.zeros(4)]).reshape(2, 2, 2) gr = get_rect_grid(top=np.ones(4), botm=botm) ix = GridIntersect(gr, method="structured") @@ -181,9 +183,8 @@ def test_rect_grid_3d_point_above(): assert len(result) == 0 +@requires_pkg("shapely") def test_rect_grid_point_outside(): - pytest.importorskip("shapely") - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") # use GeoSpatialUtil to convert to shapely geometry @@ -191,10 +192,8 @@ def test_rect_grid_point_outside(): assert len(result) == 0 +@requires_pkg("shapely") def test_rect_grid_point_on_outer_boundary(): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(Point(20.0, 10.0)) @@ -202,10 +201,8 @@ def test_rect_grid_point_on_outer_boundary(): assert np.all(result.cellids[0] == (0, 1)) +@requires_pkg("shapely") def test_rect_grid_point_on_inner_boundary(): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(Point(10.0, 10.0)) @@ -213,10 +210,8 @@ def test_rect_grid_point_on_inner_boundary(): assert np.all(result.cellids[0] == (0, 0)) +@requires_pkg("shapely") def test_rect_grid_multipoint_in_one_cell(): - pytest.importorskip("shapely") - from shapely.geometry import MultiPoint, Point - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(MultiPoint([Point(1.0, 1.0), Point(2.0, 2.0)])) @@ -224,10 +219,8 @@ def test_rect_grid_multipoint_in_one_cell(): assert result.cellids[0] == (1, 0) +@requires_pkg("shapely") def test_rect_grid_multipoint_in_multiple_cells(): - pytest.importorskip("shapely") - from shapely.geometry import MultiPoint, Point - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(MultiPoint([Point(1.0, 1.0), Point(12.0, 12.0)])) @@ -239,22 +232,18 @@ def test_rect_grid_multipoint_in_multiple_cells(): # %% test point shapely +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_point_outside_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(Point(25.0, 25.0)) assert len(result) == 0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_point_on_outer_boundary_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(Point(20.0, 10.0)) @@ -262,11 +251,9 @@ def test_rect_grid_point_on_outer_boundary_shapely(rtree): assert np.all(result.cellids[0] == (0, 1)) +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_point_on_inner_boundary_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(Point(10.0, 10.0)) @@ -274,11 +261,9 @@ def test_rect_grid_point_on_inner_boundary_shapely(rtree): assert np.all(result.cellids[0] == (0, 0)) +@requires_pkg("shapely") @rtree_toggle def test_rect_vertex_grid_point_in_one_cell_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_rect_vertex_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(Point(4.0, 4.0)) @@ -295,11 +280,9 @@ def test_rect_vertex_grid_point_in_one_cell_shapely(rtree): assert result.cellids[0] == 0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_multipoint_in_one_cell_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import MultiPoint, Point - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(MultiPoint([Point(1.0, 1.0), Point(2.0, 2.0)])) @@ -307,11 +290,9 @@ def test_rect_grid_multipoint_in_one_cell_shapely(rtree): assert result.cellids[0] == (1, 0) +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_multipoint_in_multiple_cells_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import MultiPoint, Point - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(MultiPoint([Point(1.0, 1.0), Point(12.0, 12.0)])) @@ -320,11 +301,9 @@ def test_rect_grid_multipoint_in_multiple_cells_shapely(rtree): assert result.cellids[1] == (1, 0) +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_point_outside(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_tri_grid() if gr == -1: return @@ -333,11 +312,9 @@ def test_tri_grid_point_outside(rtree): assert len(result) == 0 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_point_on_outer_boundary(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_tri_grid() if gr == -1: return @@ -347,11 +324,9 @@ def test_tri_grid_point_on_outer_boundary(rtree): assert np.all(result.cellids[0] == 0) +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_point_on_inner_boundary(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_tri_grid() if gr == -1: return @@ -361,11 +336,9 @@ def test_tri_grid_point_on_inner_boundary(rtree): assert np.all(result.cellids[0] == 0) +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_multipoint_in_one_cell(rtree): - pytest.importorskip("shapely") - from shapely.geometry import MultiPoint, Point - gr = get_tri_grid() if gr == -1: return @@ -375,11 +348,9 @@ def test_tri_grid_multipoint_in_one_cell(rtree): assert result.cellids[0] == 1 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_multipoint_in_multiple_cells(rtree): - pytest.importorskip("shapely") - from shapely.geometry import MultiPoint, Point - gr = get_tri_grid() if gr == -1: return @@ -390,11 +361,9 @@ def test_tri_grid_multipoint_in_multiple_cells(rtree): assert result.cellids[1] == 1 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_point_on_all_vertices_return_all_ix(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_rect_grid() ix = GridIntersect(gr, method="structured", rtree=rtree) n_intersections = [1, 2, 1, 2, 4, 2, 1, 2, 1] @@ -403,11 +372,9 @@ def test_rect_grid_point_on_all_vertices_return_all_ix(rtree): assert len(r) == n +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_point_on_all_vertices_return_all_ix_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) n_intersections = [1, 2, 1, 2, 4, 2, 1, 2, 1] @@ -416,11 +383,9 @@ def test_rect_grid_point_on_all_vertices_return_all_ix_shapely(rtree): assert len(r) == n +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_points_on_all_vertices_return_all_ix_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - gr = get_tri_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) n_intersections = [2, 2, 2, 2, 8, 2, 2, 2, 2] @@ -432,20 +397,16 @@ def test_tri_grid_points_on_all_vertices_return_all_ix_shapely(rtree): # %% test linestring structured +@requires_pkg("shapely") def test_rect_grid_linestring_outside(): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(25.0, 25.0), (21.0, 5.0)])) assert len(result) == 0 +@requires_pkg("shapely") def test_rect_grid_linestring_in_2cells(): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(5.0, 5.0), (15.0, 5.0)])) @@ -455,10 +416,8 @@ def test_rect_grid_linestring_in_2cells(): assert result.cellids[1] == (1, 1) +@requires_pkg("shapely") def test_rect_grid_linestring_on_outer_boundary(): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(15.0, 20.0), (5.0, 20.0)])) @@ -468,10 +427,8 @@ def test_rect_grid_linestring_on_outer_boundary(): assert result.cellids[0] == (0, 1) +@requires_pkg("shapely") def test_rect_grid_linestring_on_inner_boundary(): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(5.0, 10.0), (15.0, 10.0)])) @@ -481,10 +438,8 @@ def test_rect_grid_linestring_on_inner_boundary(): assert result.cellids[1] == (0, 1) +@requires_pkg("shapely") def test_rect_grid_multilinestring_in_one_cell(): - pytest.importorskip("shapely") - from shapely.geometry import LineString, MultiLineString - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -500,10 +455,8 @@ def test_rect_grid_multilinestring_in_one_cell(): assert result.cellids[0] == (1, 0) +@requires_pkg("shapely") def test_rect_grid_linestring_in_and_out_of_cell(): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(5.0, 9), (15.0, 5.0), (5.0, 1.0)])) @@ -513,10 +466,8 @@ def test_rect_grid_linestring_in_and_out_of_cell(): assert np.allclose(result.lengths.sum(), 21.540659228538015) +@requires_pkg("shapely") def test_rect_grid_linestring_in_and_out_of_cell2(): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -528,10 +479,8 @@ def test_rect_grid_linestring_in_and_out_of_cell2(): # assert np.allclose(result.lengths.sum(), 21.540659228538015) +@requires_pkg("shapely") def test_rect_grid_linestrings_on_boundaries_return_all_ix(): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") x, y = ix._rect_grid_to_shape_list()[0].exterior.xy @@ -542,10 +491,8 @@ def test_rect_grid_linestrings_on_boundaries_return_all_ix(): assert len(r) == n_intersections[i] +@requires_pkg("shapely") def test_rect_grid_linestring_starting_on_vertex(): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(10.0, 10.0), (15.0, 5.0)])) @@ -557,22 +504,18 @@ def test_rect_grid_linestring_starting_on_vertex(): # %% test linestring shapely +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_linestring_outside_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(LineString([(25.0, 25.0), (21.0, 5.0)])) assert len(result) == 0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_linestring_in_2cells_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(LineString([(5.0, 5.0), (15.0, 5.0)])) @@ -582,11 +525,9 @@ def test_rect_grid_linestring_in_2cells_shapely(rtree): assert result.cellids[1] == (1, 1) +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_linestring_on_outer_boundary_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(LineString([(15.0, 20.0), (5.0, 20.0)])) @@ -596,11 +537,9 @@ def test_rect_grid_linestring_on_outer_boundary_shapely(rtree): assert result.cellids[1] == (0, 1) +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_linestring_on_inner_boundary_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(LineString([(5.0, 10.0), (15.0, 10.0)])) @@ -610,11 +549,9 @@ def test_rect_grid_linestring_on_inner_boundary_shapely(rtree): assert result.cellids[1] == (0, 1) +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_multilinestring_in_one_cell_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString, MultiLineString - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect( @@ -630,11 +567,9 @@ def test_rect_grid_multilinestring_in_one_cell_shapely(rtree): assert result.cellids[0] == (1, 0) +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_linestring_in_and_out_of_cell_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(LineString([(5.0, 9), (15.0, 5.0), (5.0, 1.0)])) @@ -644,11 +579,9 @@ def test_rect_grid_linestring_in_and_out_of_cell_shapely(rtree): assert np.allclose(result.lengths.sum(), 21.540659228538015) +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_linestrings_on_boundaries_return_all_ix_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) x, y = ix._rect_grid_to_shape_list()[0].exterior.xy @@ -659,11 +592,9 @@ def test_rect_grid_linestrings_on_boundaries_return_all_ix_shapely(rtree): assert len(r) == n_intersections[i] +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_linestring_outside(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_tri_grid() if gr == -1: return @@ -672,11 +603,9 @@ def test_tri_grid_linestring_outside(rtree): assert len(result) == 0 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_linestring_in_2cells(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_tri_grid() if gr == -1: return @@ -688,11 +617,9 @@ def test_tri_grid_linestring_in_2cells(rtree): assert result.cellids[1] == 3 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_linestring_on_outer_boundary(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_tri_grid() if gr == -1: return @@ -704,11 +631,9 @@ def test_tri_grid_linestring_on_outer_boundary(rtree): assert result.cellids[1] == 7 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_linestring_on_inner_boundary(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - gr = get_tri_grid() if gr == -1: return @@ -720,11 +645,9 @@ def test_tri_grid_linestring_on_inner_boundary(rtree): assert result.cellids[1] == 1 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_multilinestring_in_one_cell(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString, MultiLineString - gr = get_tri_grid() if gr == -1: return @@ -742,11 +665,9 @@ def test_tri_grid_multilinestring_in_one_cell(rtree): assert result.cellids[0] == 4 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_linestrings_on_boundaries_return_all_ix(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - tgr = get_tri_grid() ix = GridIntersect(tgr, method="vertex", rtree=rtree) x, y = ix._vtx_grid_to_shape_list()[0].exterior.xy @@ -761,20 +682,16 @@ def test_tri_grid_linestrings_on_boundaries_return_all_ix(rtree): # %% test polygon structured +@requires_pkg("shapely") def test_rect_grid_polygon_outside(): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(Polygon([(21.0, 11.0), (23.0, 17.0), (25.0, 11.0)])) assert len(result) == 0 +@requires_pkg("shapely") def test_rect_grid_polygon_in_2cells(): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -784,10 +701,8 @@ def test_rect_grid_polygon_in_2cells(): assert result.areas.sum() == 50.0 +@requires_pkg("shapely") def test_rect_grid_polygon_on_outer_boundary(): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -796,10 +711,8 @@ def test_rect_grid_polygon_on_outer_boundary(): assert len(result) == 0 +@requires_pkg("shapely") def test_rect_grid_polygon_running_along_boundary(): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -816,10 +729,8 @@ def test_rect_grid_polygon_running_along_boundary(): ) +@requires_pkg("shapely") def test_rect_grid_polygon_on_inner_boundary(): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -829,10 +740,8 @@ def test_rect_grid_polygon_on_inner_boundary(): assert result.areas.sum() == 50.0 +@requires_pkg("shapely") def test_rect_grid_multipolygon_in_one_cell(): - pytest.importorskip("shapely") - from shapely.geometry import MultiPolygon, Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") p1 = Polygon([(1.0, 1.0), (8.0, 1.0), (8.0, 3.0), (1.0, 3.0)]) @@ -843,10 +752,8 @@ def test_rect_grid_multipolygon_in_one_cell(): assert result.areas.sum() == 28.0 +@requires_pkg("shapely") def test_rect_grid_multipolygon_in_multiple_cells(): - pytest.importorskip("shapely") - from shapely.geometry import MultiPolygon, Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") p1 = Polygon([(1.0, 1.0), (19.0, 1.0), (19.0, 3.0), (1.0, 3.0)]) @@ -857,10 +764,8 @@ def test_rect_grid_multipolygon_in_multiple_cells(): assert result.areas.sum() == 72.0 +@requires_pkg("shapely") def test_rect_grid_polygon_with_hole(): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="structured") p = Polygon( @@ -872,11 +777,9 @@ def test_rect_grid_polygon_with_hole(): assert result.areas.sum() == 104.0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_contains_centroid(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, rtree=rtree) p = Polygon( @@ -887,11 +790,9 @@ def test_rect_grid_polygon_contains_centroid(rtree): assert len(result) == 1 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_min_area(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, rtree=rtree) p = Polygon( @@ -902,10 +803,8 @@ def test_rect_grid_polygon_min_area(rtree): assert len(result) == 2 +@requires_pkg("shapely") def test_rect_grid_polygon_centroid_and_min_area(): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr) p = Polygon( @@ -919,22 +818,18 @@ def test_rect_grid_polygon_centroid_and_min_area(): # %% test polygon shapely +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_outside_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect(Polygon([(21.0, 11.0), (23.0, 17.0), (25.0, 11.0)])) assert len(result) == 0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_in_2cells_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect( @@ -944,11 +839,9 @@ def test_rect_grid_polygon_in_2cells_shapely(rtree): assert result.areas.sum() == 50.0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_on_outer_boundary_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect( @@ -957,11 +850,9 @@ def test_rect_grid_polygon_on_outer_boundary_shapely(rtree): assert len(result) == 0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_on_inner_boundary_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) result = ix.intersect( @@ -971,11 +862,9 @@ def test_rect_grid_polygon_on_inner_boundary_shapely(rtree): assert result.areas.sum() == 50.0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_multipolygon_in_one_cell_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import MultiPolygon, Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) p1 = Polygon([(1.0, 1.0), (8.0, 1.0), (8.0, 3.0), (1.0, 3.0)]) @@ -986,11 +875,9 @@ def test_rect_grid_multipolygon_in_one_cell_shapely(rtree): assert result.areas.sum() == 28.0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_multipolygon_in_multiple_cells_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import MultiPolygon, Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) p1 = Polygon([(1.0, 1.0), (19.0, 1.0), (19.0, 3.0), (1.0, 3.0)]) @@ -1001,11 +888,9 @@ def test_rect_grid_multipolygon_in_multiple_cells_shapely(rtree): assert result.areas.sum() == 72.0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_with_hole_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) p = Polygon( @@ -1017,11 +902,9 @@ def test_rect_grid_polygon_with_hole_shapely(rtree): assert result.areas.sum() == 104.0 +@requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_in_edge_in_cell(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) p = Polygon( @@ -1039,11 +922,9 @@ def test_rect_grid_polygon_in_edge_in_cell(rtree): assert result.areas.sum() == 15.0 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_polygon_outside(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_tri_grid() if gr == -1: return @@ -1052,11 +933,9 @@ def test_tri_grid_polygon_outside(rtree): assert len(result) == 0 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_polygon_in_2cells(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_tri_grid() if gr == -1: return @@ -1068,11 +947,9 @@ def test_tri_grid_polygon_in_2cells(rtree): assert result.areas.sum() == 25.0 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_polygon_on_outer_boundary(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_tri_grid() if gr == -1: return @@ -1083,11 +960,9 @@ def test_tri_grid_polygon_on_outer_boundary(rtree): assert len(result) == 0 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_polygon_on_inner_boundary(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_tri_grid() if gr == -1: return @@ -1099,11 +974,9 @@ def test_tri_grid_polygon_on_inner_boundary(rtree): assert result.areas.sum() == 50.0 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_multipolygon_in_one_cell(rtree): - pytest.importorskip("shapely") - from shapely.geometry import MultiPolygon, Polygon - gr = get_tri_grid() if gr == -1: return @@ -1116,11 +989,9 @@ def test_tri_grid_multipolygon_in_one_cell(rtree): assert result.areas.sum() == 16.5 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_multipolygon_in_multiple_cells(rtree): - pytest.importorskip("shapely") - from shapely.geometry import MultiPolygon, Polygon - gr = get_tri_grid() if gr == -1: return @@ -1133,11 +1004,9 @@ def test_tri_grid_multipolygon_in_multiple_cells(rtree): assert result.areas.sum() == 72.0 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_polygon_with_hole(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_tri_grid() if gr == -1: return @@ -1151,11 +1020,9 @@ def test_tri_grid_polygon_with_hole(rtree): assert result.areas.sum() == 104.0 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_polygon_min_area(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_tri_grid() if gr == -1: return @@ -1168,11 +1035,9 @@ def test_tri_grid_polygon_min_area(rtree): assert len(result) == 2 +@requires_pkg("shapely") @rtree_toggle def test_tri_grid_polygon_contains_centroid(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - gr = get_tri_grid() if gr == -1: return @@ -1188,10 +1053,8 @@ def test_tri_grid_polygon_contains_centroid(rtree): # %% test rotated offset grids +@requires_pkg("shapely") def test_point_offset_rot_structured_grid(): - pytest.importorskip("shapely") - from shapely.geometry import Point - sgr = get_rect_grid(angrot=45.0, xyoffset=10.0) p = Point(10.0, 10 + np.sqrt(200.0)) ix = GridIntersect(sgr, method="structured") @@ -1199,10 +1062,8 @@ def test_point_offset_rot_structured_grid(): # assert len(result) == 1. +@requires_pkg("shapely") def test_linestring_offset_rot_structured_grid(): - pytest.importorskip("shapely") - from shapely.geometry import LineString - sgr = get_rect_grid(angrot=45.0, xyoffset=10.0) ls = LineString([(5, 10.0 + np.sqrt(200.0)), (15, 10.0 + np.sqrt(200.0))]) ix = GridIntersect(sgr, method="structured") @@ -1210,10 +1071,8 @@ def test_linestring_offset_rot_structured_grid(): # assert len(result) == 2. +@requires_pkg("shapely") def test_polygon_offset_rot_structured_grid(): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - sgr = get_rect_grid(angrot=45.0, xyoffset=10.0) p = Polygon( [ @@ -1228,11 +1087,9 @@ def test_polygon_offset_rot_structured_grid(): # assert len(result) == 3. +@requires_pkg("shapely") @rtree_toggle def test_point_offset_rot_structured_grid_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Point - sgr = get_rect_grid(angrot=45.0, xyoffset=10.0) p = Point(10.0, 10 + np.sqrt(200.0)) ix = GridIntersect(sgr, method="vertex", rtree=rtree) @@ -1240,11 +1097,9 @@ def test_point_offset_rot_structured_grid_shapely(rtree): # assert len(result) == 1. +@requires_pkg("shapely") @rtree_toggle def test_linestring_offset_rot_structured_grid_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import LineString - sgr = get_rect_grid(angrot=45.0, xyoffset=10.0) ls = LineString([(5, 10.0 + np.sqrt(200.0)), (15, 10.0 + np.sqrt(200.0))]) ix = GridIntersect(sgr, method="vertex", rtree=rtree) @@ -1252,11 +1107,9 @@ def test_linestring_offset_rot_structured_grid_shapely(rtree): # assert len(result) == 2. +@requires_pkg("shapely") @rtree_toggle def test_polygon_offset_rot_structured_grid_shapely(rtree): - pytest.importorskip("shapely") - from shapely.geometry import Polygon - sgr = get_rect_grid(angrot=45.0, xyoffset=10.0) p = Polygon( [ @@ -1274,9 +1127,8 @@ def test_polygon_offset_rot_structured_grid_shapely(rtree): # %% test non strtree shapely intersect +@requires_pkg("shapely") def test_all_intersections_shapely_no_strtree(): - pytest.importorskip("shapely") - """avoid adding separate tests for rtree=False""" # Points # regular grid diff --git a/autotest/test_headufile.py b/autotest/test_headufile.py index 0616d9e875..a4ccbbfed1 100644 --- a/autotest/test_headufile.py +++ b/autotest/test_headufile.py @@ -3,9 +3,9 @@ """ import os -from shutil import which import pytest +from autotest.conftest import requires_exe, requires_pkg from flopy.discretization import UnstructuredGrid from flopy.mfusg import MfUsg, MfUsgDisU, MfUsgLpf, MfUsgSms @@ -20,12 +20,9 @@ from flopy.utils.gridgen import Gridgen -@pytest.mark.skipif( - which("mfusg") is None or which("gridgen") is None, - reason=f"requires executables: {','.join(['mfusg', 'gridgen'])}", -) +@requires_exe("mfusg", "gridgen") +@requires_pkg("shapely", "shapefile") def test_mfusg(tmpdir): - pytest.importorskip("shapely") from shapely.geometry import Polygon name = "dummy" diff --git a/autotest/test_hydmodfile.py b/autotest/test_hydmodfile.py index 77df3a8c74..a0f88d856d 100644 --- a/autotest/test_hydmodfile.py +++ b/autotest/test_hydmodfile.py @@ -3,6 +3,8 @@ import numpy as np import pytest +from autotest.conftest import has_pkg, requires_pkg + from flopy.modflow import Modflow, ModflowHyd from flopy.utils import HydmodObs, Mf6Obs @@ -116,7 +118,7 @@ def test_hydmodfile_read(hydmod_model_path): len(data.dtype.names) == nitems + 1 ), f"data column length is not {len(nitems + 1)}" - try: + if has_pkg("pandas"): import pandas as pd for idx in range(ntimes): @@ -132,13 +134,13 @@ def test_hydmodfile_read(hydmod_model_path): df = h.get_dataframe(timeunit="S") assert isinstance(df, pd.DataFrame), "A DataFrame was not returned" assert df.shape == (101, 9), "data shape is not (101, 9)" - except: + else: print("pandas not available...") pass +@requires_pkg("pandas") def test_mf6obsfile_read(mf6_obs_model_path): - pytest.importorskip("pandas") import pandas as pd txt = "binary mf6 obs" diff --git a/autotest/test_lake_connections.py b/autotest/test_lake_connections.py index d4eea89ba0..ced0431086 100644 --- a/autotest/test_lake_connections.py +++ b/autotest/test_lake_connections.py @@ -3,6 +3,8 @@ import numpy as np import pytest +from autotest.conftest import requires_pkg + from flopy.discretization import StructuredGrid from flopy.mf6 import ( MFSimulation, @@ -176,9 +178,8 @@ def test_base_run(tmpdir, example_data_path): ) +@requires_pkg("rasterio") def test_lake(tmpdir, example_data_path): - pytest.importorskip("rasterio") - mpath = example_data_path / "mf6-freyberg" top = Raster.load(str(mpath / "top.asc")) bot = Raster.load(str(mpath / "bot.asc")) diff --git a/autotest/test_listbudget.py b/autotest/test_listbudget.py index e472b00731..801429dca1 100644 --- a/autotest/test_listbudget.py +++ b/autotest/test_listbudget.py @@ -4,6 +4,8 @@ import numpy as np import pytest +from autotest.conftest import has_pkg, requires_pkg + from flopy.utils import ( Mf6ListBudget, MfListBudget, @@ -52,11 +54,11 @@ def test_mflistfile(example_data_path): cum = mflist.get_cumulative(names="PERCENT_DISCREPANCY") assert isinstance(cum, np.ndarray) - # if pandas is installed - try: - import pandas - except: + if not has_pkg("pandas"): return + + import pandas + df_flx, df_vol = mflist.get_dataframes(start_datetime=None) assert isinstance(df_flx, pandas.DataFrame) assert isinstance(df_vol, pandas.DataFrame) @@ -115,8 +117,8 @@ def test_mflist_reducedpumping_fail(example_data_path): mflist.get_reduced_pumping() +@requires_pkg("pandas") def test_mtlist(example_data_path): - pytest.importorskip("pandas") import pandas as pd mt_dir = str(example_data_path / "mt3d_test") diff --git a/autotest/test_mnw.py b/autotest/test_mnw.py index 3e2eb068e5..33a5f2b129 100644 --- a/autotest/test_mnw.py +++ b/autotest/test_mnw.py @@ -4,6 +4,8 @@ import numpy as np import pytest +from autotest.conftest import requires_pkg + from flopy.modflow import Mnw, Modflow, ModflowDis, ModflowMnw2 """ @@ -292,12 +294,12 @@ def test_make_package(tmpdir): ) +@requires_pkg("pandas") def test_mnw2_create_file(tmpdir): """ Test for issue #556, Mnw2 crashed if wells have multiple node lengths """ - pytest.importorskip("pandas") import pandas as pd mf = Modflow("test_mfmnw2", exe_name="mf2005") @@ -364,10 +366,10 @@ def test_mnw2_create_file(tmpdir): mnw2.write_file(os.path.join(ws, "ndata.mnw2")) +@requires_pkg("netCDF4") @pytest.mark.slow def test_export(tmpdir, mnw2_examples_path): """t027 test export of MNW2 Package to netcdf files""" - pytest.importorskip("netCDF4") import netCDF4 ws = str(tmpdir) diff --git a/autotest/test_modflow.py b/autotest/test_modflow.py index 0347c82e0f..861b82bb49 100644 --- a/autotest/test_modflow.py +++ b/autotest/test_modflow.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from autotest.conftest import get_example_data_path, requires_exe +from autotest.conftest import get_example_data_path, requires_exe, requires_pkg from matplotlib import pyplot as plt from flopy.discretization import StructuredGrid @@ -938,6 +938,7 @@ def test_rchload(tmpdir): assert np.allclose(a1, a2) +@requires_pkg("pandas") def test_mp5_load(tmpdir, example_data_path): # load the base freyberg model freyberg_ws = example_data_path / "freyberg" @@ -995,6 +996,7 @@ def test_mp5_load(tmpdir, example_data_path): plt.close() +@requires_pkg("pandas") def test_mp5_timeseries_load(example_data_path): pth = str(example_data_path / "mp5") files = [ @@ -1007,6 +1009,7 @@ def test_mp5_timeseries_load(example_data_path): eval_timeseries(file) +@requires_pkg("pandas") def test_mp6_timeseries_load(example_data_path): pth = str(example_data_path / "mp5") files = [ diff --git a/autotest/test_modpathfile.py b/autotest/test_modpathfile.py index d4672f53cd..78d89c5298 100644 --- a/autotest/test_modpathfile.py +++ b/autotest/test_modpathfile.py @@ -10,7 +10,7 @@ from flopy.modpath import Modpath7 from flopy.utils import PathlineFile, EndpointFile -from autotest.conftest import requires_exes +from autotest.conftest import requires_exe @pytest.fixture(scope="session") @@ -170,7 +170,7 @@ def get_nodes(locs): -@requires_exes(["mf6", "mp7"]) +@requires_exe("mf6", "mp7") @pytest.mark.skip(reason="pending https://github.com/modflowpy/flopy/issues/1479") @pytest.mark.slow @pytest.mark.parametrize("direction", ["forward", "backward"]) @@ -195,7 +195,7 @@ def test_get_destination_pathline_data(tmpdir, mp7_simulation, direction, locati pathline_data = benchmark(lambda: pathline_file.get_destination_pathline_data(dest_cells=nodew if locations == "well" else nodesr)) -@requires_exes(["mf6", "mp7"]) +@requires_exe("mf6", "mp7") @pytest.mark.parametrize("direction", ["forward", "backward"]) @pytest.mark.parametrize("locations", ["well", "river"]) def test_get_destination_endpoint_data(tmpdir, mp7_simulation, direction, locations, benchmark): @@ -226,7 +226,7 @@ def profile_outdir(request, project_root_path): return project_root_path / "autotest" / ".profile" if autosave else None -@requires_exes(["mf6", "mp7"]) +@requires_exe("mf6", "mp7") @pytest.mark.profile @pytest.mark.parametrize("direction", ["forward", "backward"]) @pytest.mark.parametrize("locations", ["well", "river"]) @@ -265,7 +265,7 @@ def test_profile_get_destination_pathline_data(tmpdir, profile_outdir, mp7_simul f.write(s.getvalue()) -@requires_exes(["mf6", "mp7"]) +@requires_exe("mf6", "mp7") @pytest.mark.profile @pytest.mark.parametrize("direction", ["forward", "backward"]) @pytest.mark.parametrize("locations", ["well", "river"]) diff --git a/autotest/test_mp6.py b/autotest/test_mp6.py index 0498f78e0f..6b019022a4 100644 --- a/autotest/test_mp6.py +++ b/autotest/test_mp6.py @@ -1,11 +1,10 @@ import os import shutil -from shutil import which import matplotlib.pyplot as plt import numpy as np import pytest -from autotest.conftest import get_example_data_path +from autotest.conftest import get_example_data_path, requires_exe, requires_pkg from flopy.discretization import StructuredGrid from flopy.export.shapefile_utils import shp2recarray @@ -157,9 +156,8 @@ def test_mpsim(tmpdir, mp6_test_path): assert stllines[6].strip().split()[-1] == "p2" +@requires_pkg("pandas", "shapefile") def test_get_destination_data(tmpdir, mp6_test_path): - pytest.importorskip("shapefile") - copy_modpath_files(str(mp6_test_path), str(tmpdir), "EXAMPLE.") copy_modpath_files(str(mp6_test_path), str(tmpdir), "EXAMPLE-3.") @@ -332,6 +330,7 @@ def test_get_destination_data(tmpdir, mp6_test_path): pthobj.write_shapefile(shpname=fpth, direction="ending", mg=mg4) +@requires_pkg("pandas") def test_loadtxt(tmpdir, mp6_test_path): copy_modpath_files(str(mp6_test_path), str(tmpdir), "EXAMPLE-3.") @@ -347,9 +346,8 @@ def test_loadtxt(tmpdir, mp6_test_path): # epd = EndpointFile(epfilewithnans) -@pytest.mark.skipif( - which("mf2005") is None, reason="requires mf2005 executable" -) +@requires_exe("mf2005") +@requires_pkg("pandas") def test_modpath(tmpdir, example_data_path): pth = example_data_path / "freyberg" mfnam = "freyberg.nam" @@ -751,10 +749,7 @@ def case_mf2005(tmpdir): return mp -@pytest.mark.skipif( - any(which(exe) is None for exe in exe_names), - reason=f"requires executables: {exe_names}", -) +@requires_exe(*exe_names) def test_pathline_output(case_mf2005, case_mf6): success, buff = case_mf2005.run_model() assert success, f"modpath model ({case_mf2005.name}) did not run" @@ -781,6 +776,7 @@ def test_pathline_output(case_mf2005, case_mf6): assert maxid0 == maxid1, msg +@requires_pkg("pandas") def test_endpoint_output(case_mf2005, case_mf6): success, buff = case_mf2005.run_model() assert success, f"modpath model ({case_mf2005.name}) did not run" diff --git a/autotest/test_mp7.py b/autotest/test_mp7.py index 12015fbc01..4ed7c97c66 100644 --- a/autotest/test_mp7.py +++ b/autotest/test_mp7.py @@ -2,7 +2,7 @@ import numpy as np import pytest -from autotest.conftest import requires_exe +from autotest.conftest import requires_exe, requires_pkg from flopy.mf6 import ( MFSimulation, @@ -230,7 +230,7 @@ def endpoint_compare(fpth0, epf): assert np.allclose(t0[name], t1[name]), msg -@requires_exe("mp7") +@requires_exe("mf6", "mp7") def test_default_modpath(ex01b_mf6_model): sim, tmpdir = ex01b_mf6_model @@ -244,7 +244,8 @@ def test_default_modpath(ex01b_mf6_model): ) -@requires_exe("mp7") +@requires_exe("mf6", "mp7") +@requires_pkg("pandas") def test_faceparticles_is1(ex01b_mf6_model): sim, tmpdir = ex01b_mf6_model @@ -302,7 +303,7 @@ def test_faceparticles_is1(ex01b_mf6_model): endpoint_compare(fpth0, epf) -@requires_exe("mp7") +@requires_exe("mf6", "mp7") def test_facenode_is3(ex01b_mf6_model): sim, tmpdir = ex01b_mf6_model grid = sim.get_model(ex01b_mf6_model_name).modelgrid @@ -340,7 +341,7 @@ def test_facenode_is3(ex01b_mf6_model): ) -@requires_exe("mp7") +@requires_exe("mf6", "mp7") def test_facenode_is3a(ex01b_mf6_model): sim, tmpdir = ex01b_mf6_model grid = sim.get_model(ex01b_mf6_model_name).modelgrid @@ -383,7 +384,7 @@ def test_facenode_is3a(ex01b_mf6_model): ) -@requires_exe("mp7") +@requires_exe("mf6", "mp7") def test_facenode_is2a(ex01b_mf6_model): sim, tmpdir = ex01b_mf6_model grid = sim.get_model(ex01b_mf6_model_name).modelgrid @@ -418,7 +419,8 @@ def test_facenode_is2a(ex01b_mf6_model): ) -@requires_exe("mp7") +@requires_exe("mf6", "mp7") +@requires_pkg("pandas") def test_cellparticles_is1(ex01b_mf6_model): sim, tmpdir = ex01b_mf6_model grid = sim.get_model(ex01b_mf6_model_name).modelgrid @@ -457,7 +459,7 @@ def test_cellparticles_is1(ex01b_mf6_model): endpoint_compare(fpth0, epf) -@requires_exe("mp7") +@requires_exe("mf6", "mp7") def test_cellparticleskij_is1(ex01b_mf6_model): sim, tmpdir = ex01b_mf6_model grid = sim.get_model(ex01b_mf6_model_name).modelgrid @@ -482,7 +484,7 @@ def test_cellparticleskij_is1(ex01b_mf6_model): ) -@requires_exe("mp7") +@requires_exe("mf6", "mp7") def test_cellnode_is3(ex01b_mf6_model): sim, tmpdir = ex01b_mf6_model grid = sim.get_model(ex01b_mf6_model_name).modelgrid @@ -512,7 +514,7 @@ def test_cellnode_is3(ex01b_mf6_model): ) -@requires_exe("mp7") +@requires_exe("mf6", "mp7") def test_cellnode_is3a(ex01b_mf6_model): sim, tmpdir = ex01b_mf6_model grid = sim.get_model(ex01b_mf6_model_name).modelgrid @@ -556,7 +558,7 @@ def test_cellnode_is3a(ex01b_mf6_model): ) -@requires_exe("mp7") +@requires_exe("mf6", "mp7") def test_cellnode_is2a(ex01b_mf6_model): sim, tmpdir = ex01b_mf6_model grid = sim.get_model(ex01b_mf6_model_name).modelgrid @@ -686,7 +688,7 @@ def ex01_mf6_model(tmpdir): @pytest.mark.slow -@requires_exe("mp7") +@requires_exe("mf6", "mp7") def test_forward(ex01_mf6_model): sim, tmpdir = ex01_mf6_model # Run the simulation @@ -719,7 +721,7 @@ def test_forward(ex01_mf6_model): @pytest.mark.slow -@requires_exe("mp7") +@requires_exe("mf6", "mp7") def test_backward(ex01_mf6_model): sim, tmpdir = ex01_mf6_model success, buff = sim.run_simulation() diff --git a/autotest/test_mt3d.py b/autotest/test_mt3d.py index 396c159ce3..d678e744e6 100644 --- a/autotest/test_mt3d.py +++ b/autotest/test_mt3d.py @@ -7,7 +7,7 @@ from autotest.conftest import ( excludes_platform, - requires_exes, + requires_exe, ) from flopy.modflow import ( Modflow, @@ -61,7 +61,7 @@ def mfnwtmt3d_model_path(mt3d_test_model_path): return mt3d_test_model_path / "mfnwt_mt3dusgs" -@requires_exes(["mf2005", "mt3dms"]) +@requires_exe("mf2005", "mt3dms") def test_mf2005_p07(tmpdir, mf2005mt3d_model_path): pth = str(mf2005mt3d_model_path / "P07") namfile = "p7mf2005.nam" @@ -92,7 +92,7 @@ def test_mf2005_p07(tmpdir, mf2005mt3d_model_path): os.remove(os.path.join(cpth, ftlfile)) -@requires_exes(["mf2000", "mt3dms"]) +@requires_exe("mf2000", "mt3dms") def test_mf2000_p07(tmpdir, mf2kmt3d_model_path): pth = str(mf2kmt3d_model_path / "P07") namfile = "p7mf2k.nam" @@ -117,7 +117,7 @@ def test_mf2000_p07(tmpdir, mf2kmt3d_model_path): os.remove(os.path.join(cpth, ftlfile)) -@requires_exes(["mf2000", "mt3dms"]) +@requires_exe("mf2000", "mt3dms") def test_mf2000_HSSTest(tmpdir, mf2kmt3d_model_path): pth = str(mf2kmt3d_model_path / "HSSTest") namfile = "hsstest_mf2k.nam" @@ -144,7 +144,7 @@ def test_mf2000_HSSTest(tmpdir, mf2kmt3d_model_path): os.remove(os.path.join(cpth, ftlfile)) -@requires_exes(["mf2000", "mt3dms"]) +@requires_exe("mf2000", "mt3dms") def test_mf2000_mnw(tmpdir, mf2kmt3d_model_path): # cannot run this model because it uses mnw1 and there is no load for mnw1 # this model includes block format data in the btn file @@ -163,7 +163,7 @@ def test_mf2000_mnw(tmpdir, mf2kmt3d_model_path): mt.write_input() -@requires_exes(["mf2000", "mt3dms"]) +@requires_exe("mf2000", "mt3dms") def test_mf2000_MultiDiffusion(tmpdir, mf2kmt3d_model_path): pth = str(mf2kmt3d_model_path / "MultiDiffusion") namfile = "p7mf2k.nam" @@ -188,7 +188,7 @@ def test_mf2000_MultiDiffusion(tmpdir, mf2kmt3d_model_path): os.remove(os.path.join(cpth, ftlfile)) -@requires_exes(["mf2000", "mt3dms"]) +@requires_exe("mf2000", "mt3dms") def test_mf2000_reinject(tmpdir, mf2kmt3d_model_path): pth = str(mf2kmt3d_model_path / "reinject") namfile = "p3mf2k.nam" @@ -215,7 +215,7 @@ def test_mf2000_reinject(tmpdir, mf2kmt3d_model_path): os.remove(os.path.join(cpth, ftlfile)) -@requires_exes(["mf2000", "mt3dms"]) +@requires_exe("mf2000", "mt3dms") def test_mf2000_SState(tmpdir, mf2kmt3d_model_path): pth = str(mf2kmt3d_model_path / "SState") namfile = "SState_mf2k.nam" @@ -242,7 +242,7 @@ def test_mf2000_SState(tmpdir, mf2kmt3d_model_path): os.remove(os.path.join(cpth, ftlfile)) -@requires_exes(["mf2000", "mt3dms"]) +@requires_exe("mf2000", "mt3dms") def test_mf2000_tob(tmpdir, mf2kmt3d_model_path): pth = str(mf2kmt3d_model_path / "tob") namfile = "p7mf2k.nam" @@ -272,7 +272,7 @@ def test_mf2000_tob(tmpdir, mf2kmt3d_model_path): os.remove(os.path.join(cpth, ftlfile)) -@requires_exes(["mf2000", "mt3dms"]) +@requires_exe("mf2000", "mt3dms") def test_mf2000_zeroth(tmpdir, mf2kmt3d_model_path): pth = str(mf2kmt3d_model_path / "zeroth") namfile = "z0mf2k.nam" @@ -299,7 +299,7 @@ def test_mf2000_zeroth(tmpdir, mf2kmt3d_model_path): @flaky -@requires_exes(["mfnwt", "mt3dms"]) +@requires_exe("mfnwt", "mt3dms") @excludes_platform("Windows", ci_only=True) # TODO remove once fixed in MT3D-USGS def test_mfnwt_CrnkNic(tmpdir, mfnwtmt3d_model_path): pth = str(mfnwtmt3d_model_path / "sft_crnkNic") @@ -339,7 +339,7 @@ def test_mfnwt_CrnkNic(tmpdir, mfnwtmt3d_model_path): @pytest.mark.slow -@requires_exes(["mfnwt", "mt3dusgs"]) +@requires_exe("mfnwt", "mt3dusgs") def test_mfnwt_LKT(tmpdir, mfnwtmt3d_model_path): pth = str(mfnwtmt3d_model_path / "lkt") namefile = "lkt_mf.nam" @@ -387,7 +387,7 @@ def test_mfnwt_LKT(tmpdir, mfnwtmt3d_model_path): @pytest.mark.slow -@requires_exes(["mfnwt", "mt3dusgs"]) +@requires_exe("mfnwt", "mt3dusgs") def test_mfnwt_keat_uzf(tmpdir, mfnwtmt3d_model_path): pth = str(mfnwtmt3d_model_path / "keat_uzf") namefile = "Keat_UZF_mf.nam" @@ -656,7 +656,7 @@ def test_mt3d_multispecies(tmpdir): mt2.write_input() -@requires_exes(["mfnwt", "mt3dusgs"]) +@requires_exe("mfnwt", "mt3dusgs") def test_lkt_with_multispecies(tmpdir): # Bug discovered in LKT with multi-species. Adding test to check this functionality diff --git a/autotest/test_obs.py b/autotest/test_obs.py index 977a0bc5da..e632b16905 100644 --- a/autotest/test_obs.py +++ b/autotest/test_obs.py @@ -1,10 +1,11 @@ import os import shutil -from shutil import which import numpy as np import pytest +from autotest.conftest import requires_exe + from flopy.modflow import ( HeadObservation, Modflow, @@ -18,9 +19,7 @@ ) -@pytest.mark.skipif( - which("mf2005") is None, reason="requires mf2005 executable" -) +@requires_exe("mf2005") def test_hob_simple(tmpdir): """ test041 create and run a simple MODFLOW-2005 OBS example @@ -107,9 +106,7 @@ def test_hob_simple(tmpdir): assert success, "could not run simple MODFLOW-2005 model" -@pytest.mark.skipif( - which("mf2005") is None, reason="requires mf2005 executable" -) +@requires_exe("mf2005") def test_obs_load_and_write(tmpdir, example_data_path): """ test041 load and write of MODFLOW-2005 OBS example problem diff --git a/autotest/test_plot.py b/autotest/test_plot.py index a62c22f845..0e77a24625 100644 --- a/autotest/test_plot.py +++ b/autotest/test_plot.py @@ -12,6 +12,8 @@ import pytest from flaky import flaky +from autotest.conftest import requires_pkg + import flopy from flopy.discretization import StructuredGrid from flopy.mf6 import MFSimulation @@ -293,6 +295,7 @@ def test_model_dot_plot(tmpdir, example_data_path): plt.close("all") +@requires_pkg("pandas") def test_pathline_plot_xc(tmpdir, example_data_path): # test with multi-layer example load_ws = example_data_path / "mp6" diff --git a/autotest/test_scripts.py b/autotest/test_scripts.py index 2b06f72d08..195b5432c5 100644 --- a/autotest/test_scripts.py +++ b/autotest/test_scripts.py @@ -1,7 +1,6 @@ """Test scripts.""" import sys import urllib -from subprocess import PIPE, Popen from urllib.error import HTTPError import pytest @@ -10,6 +9,7 @@ from autotest.conftest import ( get_project_root_path, requires_github, + run_py_script, ) flopy_dir = get_project_root_path(__file__) @@ -22,18 +22,6 @@ def downloads_dir(tmp_path_factory): return downloads_dir -def run_py_script(script, *args): - """Run a Python script, return tuple (stdout, stderr, returncode).""" - args = [sys.executable, str(script)] + [str(g) for g in args] - print("running: " + " ".join(args)) - p = Popen(args, stdout=PIPE, stderr=PIPE) - stdout, stderr = p.communicate() - stdout = stdout.decode() - stderr = stderr.decode() - returncode = p.returncode - return stdout, stderr, returncode - - def run_get_modflow_script(*args): return run_py_script(get_modflow_script, *args) diff --git a/autotest/test_sfr.py b/autotest/test_sfr.py index 501f738bf4..ed4ac99513 100644 --- a/autotest/test_sfr.py +++ b/autotest/test_sfr.py @@ -3,13 +3,12 @@ import os import shutil from pathlib import Path -from shutil import which import matplotlib import matplotlib.pyplot as plt import numpy as np import pytest -from autotest.conftest import get_example_data_path, requires_exe +from autotest.conftest import get_example_data_path, requires_exe, requires_pkg from flopy.discretization import StructuredGrid from flopy.modflow import Modflow, ModflowDis, ModflowSfr2, ModflowStr @@ -195,11 +194,10 @@ def sfr_process(mfnam, sfrfile, model_ws, outfolder): "UZFtest2.nam", "UZFtest2.sfr", mf2005_model_path, tmpdir ) - if matplotlib is not None: - assert isinstance( - sfr.plot()[0], matplotlib.axes.Axes - ) # test the plot() method - matplotlib.pyplot.close() + assert isinstance( + sfr.plot()[0], matplotlib.axes.Axes + ) # test the plot() method + matplotlib.pyplot.close() def interpolate_to_reaches(sfr): reach_data = sfr.reach_data @@ -374,9 +372,8 @@ def test_const(sfr_data): assert True +@requires_pkg("pandas", "shapefile") def test_export(tmpdir, sfr_data): - pytest.importorskip("shapefile") - m = Modflow() dis = ModflowDis(m, 1, 10, 10, lenuni=2, itmuni=4) @@ -667,6 +664,7 @@ def test_assign_layers(tmpdir): assert np.array_equal(l, np.array([1, 1])) +@requires_pkg("pandas") def test_SfrFile(tmpdir, sfr_examples_path, mf2005_model_path): common_names = [ "layer", diff --git a/autotest/test_str.py b/autotest/test_str.py index f36e98c2e2..5cafdc0a07 100644 --- a/autotest/test_str.py +++ b/autotest/test_str.py @@ -1,6 +1,6 @@ import matplotlib -from autotest.conftest import requires_exe +from autotest.conftest import requires_exe, requires_pkg from flopy.modflow import Modflow from flopy.utils import MfListBudget @@ -16,6 +16,7 @@ @requires_exe("mf2005") +@requires_pkg("pandas") def test_str_issue1164(tmpdir, example_data_path): mf2005_model_path = example_data_path / "mf2005_test" m = Modflow.load( @@ -65,6 +66,5 @@ def test_str_plot(example_data_path): verbose=True, check=False, ) - if matplotlib is not None: - assert isinstance(m.str.plot()[0], matplotlib.axes.Axes) - matplotlib.pyplot.close() + assert isinstance(m.str.plot()[0], matplotlib.axes.Axes) + matplotlib.pyplot.close() diff --git a/autotest/test_swr_binaryread.py b/autotest/test_swr_binaryread.py index 23461796c0..34a284a18f 100644 --- a/autotest/test_swr_binaryread.py +++ b/autotest/test_swr_binaryread.py @@ -1,8 +1,8 @@ # Test SWR binary read functionality -import os - import pytest +from autotest.conftest import has_pkg + from flopy.utils import ( SwrBudget, SwrExchange, @@ -447,7 +447,7 @@ def test_swr_binary_obs(swr_test_path, ipos): ), "SwrObs data does not have nobs + 1" # test get_dataframes() - try: + if has_pkg("pandas"): import pandas as pd for idx in range(ntimes): @@ -463,5 +463,5 @@ def test_swr_binary_obs(swr_test_path, ipos): df = sobj.get_dataframe(timeunit="S") assert isinstance(df, pd.DataFrame), "A DataFrame was not returned" assert df.shape == (336, nobs + 1), "data shape is not (336, 10)" - except ImportError: + else: print("pandas not available...") diff --git a/autotest/test_util_2d_and_3d.py b/autotest/test_util_2d_and_3d.py index 92deaa1705..980286d219 100644 --- a/autotest/test_util_2d_and_3d.py +++ b/autotest/test_util_2d_and_3d.py @@ -1,6 +1,9 @@ import os import numpy as np +import pytest + +from autotest.conftest import requires_pkg from flopy.modflow import ( Modflow, @@ -433,6 +436,7 @@ def test_append_mflist(tmpdir): ml.write_input() +@requires_pkg("pandas") def test_mflist(tmpdir, example_data_path): model = Modflow(model_ws=str(tmpdir)) dis = ModflowDis(model, 10, 10, 10, 10) @@ -602,6 +606,7 @@ def test_util3d_reset(): ml.bas6.strt = arr +@requires_pkg("pandas") def test_mflist_fromfile(tmpdir): """test that when a file is passed to stress period data, the .array attribute will load the file diff --git a/autotest/test_uzf.py b/autotest/test_uzf.py index 197c1685dd..06230a9211 100644 --- a/autotest/test_uzf.py +++ b/autotest/test_uzf.py @@ -1,12 +1,12 @@ -import glob import os import shutil from io import StringIO -from shutil import which import numpy as np import pytest +from autotest.conftest import requires_exe + from flopy.modflow import ( Modflow, ModflowBas, @@ -243,7 +243,7 @@ def test_create_uzf(tmpdir, mf2005_test_path, uzf_test_path): ) -@pytest.mark.skipif(which("mfnwt") is None, reason="requires mfnwt executable") +@requires_exe("mfnwt") def test_uzf_surfk(tmpdir, uzf_test_path): ws = str(uzf_test_path) uzf_name = "UZFtest4.uzf" diff --git a/autotest/test_zonbud_utility.py b/autotest/test_zonbud_utility.py index 5a25ff0e3a..f34e0aa3f5 100644 --- a/autotest/test_zonbud_utility.py +++ b/autotest/test_zonbud_utility.py @@ -3,6 +3,8 @@ import numpy as np import pytest +from autotest.conftest import requires_pkg + from flopy.mf6 import MFSimulation from flopy.utils import ZoneBudget, ZoneBudget6, ZoneFile6 @@ -206,10 +208,8 @@ def test_zonbud_readwrite_zbarray(tmpdir): assert np.array_equal(x, z), "Input and output arrays do not match." +@requires_pkg("pandas") def test_dataframes(cbc_f, zon_f): - pytest.importorskip("pandas") - import pandas as pd - zon = ZoneBudget.read_zone_file(str(zon_f)) cmd = ZoneBudget(str(cbc_f), zon, totim=1095.0) df = cmd.get_dataframes() @@ -234,9 +234,9 @@ def test_get_model_shape(cbc_f, zon_f): ).get_model_shape() +@requires_pkg("pandas") @pytest.mark.parametrize("rtol", [1e-2]) def test_zonbud_active_areas_zone_zero(loadpth, cbc_f, rtol): - pytest.importorskip("pandas") import pandas as pd # Read ZoneBudget executable output and reformat @@ -283,8 +283,8 @@ def test_read_zone_file(tmpdir): raise AssertionError("zone file read failed") +@requires_pkg("pandas") def test_zonebudget_6(tmpdir, example_data_path): - pytest.importorskip("pandas") import pandas as pd exe_name = "mf6" diff --git a/etc/environment.yml b/etc/environment.yml index 5a644c3357..49b5319d70 100644 --- a/etc/environment.yml +++ b/etc/environment.yml @@ -15,14 +15,14 @@ dependencies: # test - coverage - - pytest - - pytest-cov - - pytest-xdist - - pytest-benchmark - flaky - filelock - jupyter - jupytext + - pytest + - pytest-cov + - pytest-xdist + - pytest-benchmark # optional - appdirs diff --git a/setup.cfg b/setup.cfg index 61301d45cf..b3700b6b77 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,15 +45,15 @@ lint = test = %(lint)s coverage - mfpymake - pytest - pytest-cov - pytest-xdist - pytest-benchmark flaky filelock jupyter jupytext + mfpymake + pytest + pytest-benchmark + pytest-cov + pytest-xdist optional = affine appdirs