diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efdb4a4..ea7e671 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,8 @@ jobs: defaults: run: shell: bash - steps: - # check out repo - name: Checkout repo uses: actions/checkout@v3 @@ -45,19 +43,16 @@ jobs: run: | python -c "import modflowapi; print(modflowapi.__version__)" - lint: name: lint runs-on: ubuntu-latest + if: github.event_name != 'schedule' strategy: fail-fast: false defaults: run: shell: bash - - if: github.event_name != 'schedule' steps: - # check out repo - name: Checkout repo uses: actions/checkout@v3 @@ -78,20 +73,11 @@ jobs: python -m pip install --upgrade pip pip install -e .[lint] - - name: Run black - run: | - echo "if black check fails run" - echo " black ./modflowapi" - echo "and then commit the changes." - black --check ./modflowapi - - - name: Run flake8 - run: | - flake8 --count --show-source --exit-zero ./modflowapi + - name: Lint + run: ruff check . - - name: Run pylint - run: | - pylint --jobs=2 --errors-only --exit-zero ./modflowapi + - name: Check format + run: ruff format . --check autotest_extensions: name: modflowapi extensions autotests @@ -105,9 +91,7 @@ jobs: defaults: run: shell: bash - steps: - # check out repo - name: Checkout repo uses: actions/checkout@v3 @@ -148,9 +132,7 @@ jobs: defaults: run: shell: bash - steps: - # check out repo - name: Checkout repo uses: actions/checkout@v3 @@ -192,9 +174,7 @@ jobs: defaults: run: shell: bash - steps: - # check out repo - name: Checkout repo uses: actions/checkout@v3 @@ -212,12 +192,12 @@ jobs: pip install git+https://git@github.com/MODFLOW-USGS/modflow-devtools@develop pip install .[test] - - name: Install modflow executables + - name: Install modflow6 nightly build uses: modflowpy/install-modflow-action@v1 with: path: ${{ github.workspace }}/autotest repo: modflow6-nightly-build - + - name: Run autotests working-directory: ./autotest shell: bash -l {0} diff --git a/.gitignore b/.gitignore index a0b56cf..6a6e2e1 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,8 @@ dmypy.json # pycharm .idea/ + +# library files +**.dll +**.so +**.dylib \ No newline at end of file diff --git a/autotest/conftest.py b/autotest/conftest.py index e417e43..d059061 100644 --- a/autotest/conftest.py +++ b/autotest/conftest.py @@ -6,6 +6,11 @@ import pytest from filelock import FileLock +from modflow_devtools.download import download_and_unzip + +# import modflow-devtools fixtures +pytest_plugins = ["modflow_devtools.fixtures"] + __mf6_examples = "mf6_examples" __mf6_examples_path = Path(gettempdir()) / __mf6_examples @@ -13,9 +18,6 @@ def get_mf6_examples_path() -> Path: - pytest.importorskip("modflow_devtools") - from modflow_devtools.download import download_and_unzip - # use file lock so mf6 distribution is downloaded once, # even when tests are run in parallel with pytest-xdist __mf6_examples_lock.acquire() @@ -95,45 +97,3 @@ def simulation_name_from_model_namfiles(mnams): metafunc.parametrize( key, simulations, ids=simulation_name_from_model_namfiles ) - - -@pytest.fixture(scope="function") -def tmpdir(tmpdir_factory, request) -> Path: - node = ( - request.node.name.replace("/", "_") - .replace("\\", "_") - .replace(":", "_") - ) - temp = Path(tmpdir_factory.mktemp(node)) - yield Path(temp) - - keep = request.config.getoption("--keep") - if keep: - copytree(temp, Path(keep) / temp.name) - - keep_failed = request.config.getoption("--keep-failed") - if keep_failed and request.node.rep_call.failed: - copytree(temp, Path(keep_failed) / temp.name) - - -def pytest_addoption(parser): - parser.addoption( - "-K", - "--keep", - action="store", - default=None, - help="Move the contents of temporary test directories to correspondingly named subdirectories at the given " - "location after tests complete. This option can be used to exclude test results from automatic cleanup, " - "e.g. for manual inspection. The provided path is created if it does not already exist. An error is " - "thrown if any matching files already exist.", - ) - - parser.addoption( - "--keep-failed", - action="store", - default=None, - help="Move the contents of temporary test directories to correspondingly named subdirectories at the given " - "location if the test case fails. This option automatically saves the outputs of failed tests in the " - "given location. The path is created if it doesn't already exist. An error is thrown if files with the " - "same names already exist in the given location.", - ) diff --git a/autotest/test_interface.py b/autotest/test_interface.py index 5027218..8119a5f 100644 --- a/autotest/test_interface.py +++ b/autotest/test_interface.py @@ -11,7 +11,6 @@ AdvancedPackage, ArrayPackage, ListPackage, - pkgvars, ) data_pth = Path("../examples/data") @@ -20,7 +19,11 @@ so = "libmf6" + ( ".so" if os == "Linux" - else ".dylib" if os == "Darwin" else ".dll" if os == "Windows" else None + else ".dylib" + if os == "Darwin" + else ".dll" + if os == "Windows" + else None ) if so is None: pytest.skip("Unsupported operating system", allow_module_level=True) @@ -28,27 +31,27 @@ @pytest.mark.parametrize("use_str", [True, False]) def test_ctor_finds_libmf6_by_name(use_str): - api = ModflowApi(so if use_str else Path(so)) + ModflowApi(so if use_str else Path(so)) @pytest.mark.parametrize("use_str", [True, False]) -def test_ctor_finds_libmf6_by_relpath(tmpdir, use_str): - shutil.copy(so, tmpdir) - inner = tmpdir / "inner" +def test_ctor_finds_libmf6_by_relpath(function_tmpdir, use_str): + shutil.copy(so, function_tmpdir) + inner = function_tmpdir / "inner" inner.mkdir() with set_dir(inner): so_path = f"../{so}" - api = ModflowApi(so_path if use_str else Path(so_path)) + ModflowApi(so_path if use_str else Path(so_path)) @pytest.mark.parametrize("use_str", [True, False]) -def test_ctor_finds_libmf6_by_abspath(tmpdir, use_str): - shutil.copy(so, tmpdir) - so_path = tmpdir / so - api = ModflowApi(str(so_path) if use_str else so_path) +def test_ctor_finds_libmf6_by_abspath(function_tmpdir, use_str): + shutil.copy(so, function_tmpdir) + so_path = function_tmpdir / so + ModflowApi(str(so_path) if use_str else so_path) -def test_dis_model(tmpdir): +def test_dis_model(function_tmpdir): def callback(sim, step): """ Callback function @@ -134,7 +137,7 @@ def callback(sim, step): name = "dis_model" sim_pth = data_pth / name - test_pth = tmpdir / name + test_pth = function_tmpdir / name shutil.copytree(sim_pth, test_pth, dirs_exist_ok=True) try: @@ -143,7 +146,7 @@ def callback(sim, step): raise Exception(e) -def test_disv_model(tmpdir): +def test_disv_model(function_tmpdir): def callback(sim, step): """ Callback function @@ -220,7 +223,7 @@ def callback(sim, step): name = "disv_model" sim_pth = data_pth / name - test_pth = tmpdir / name + test_pth = function_tmpdir / name shutil.copytree(sim_pth, test_pth, dirs_exist_ok=True) try: @@ -229,7 +232,7 @@ def callback(sim, step): raise Exception(e) -def test_disu_model(tmpdir): +def test_disu_model(function_tmpdir): def callback(sim, step): """ Callback function @@ -300,7 +303,7 @@ def callback(sim, step): name = "disu_model" sim_pth = data_pth / name - test_pth = tmpdir / name + test_pth = function_tmpdir / name shutil.copytree(sim_pth, test_pth, dirs_exist_ok=True) try: @@ -309,7 +312,7 @@ def callback(sim, step): raise Exception(e) -def test_two_models(tmpdir): +def test_two_models(function_tmpdir): def callback(sim, step): """ Callback function @@ -326,7 +329,7 @@ def callback(sim, step): name = "two_models" sim_pth = data_pth / name - test_pth = tmpdir / name + test_pth = function_tmpdir / name shutil.copytree(sim_pth, test_pth, dirs_exist_ok=True) try: @@ -335,7 +338,7 @@ def callback(sim, step): raise Exception(e) -def test_ats_model(tmpdir): +def test_ats_model(function_tmpdir): def callback(sim, step): if step == Callbacks.stress_period_start: if sim.kper == 0 and sim.kstp == 0: @@ -350,7 +353,7 @@ def callback(sim, step): name = "ats0" sim_pth = data_pth / name - test_pth = tmpdir / name + test_pth = function_tmpdir / name shutil.copytree(sim_pth, test_pth, dirs_exist_ok=True) try: @@ -359,7 +362,7 @@ def callback(sim, step): raise Exception(e) -def test_rhs_hcof_advanced(tmpdir): +def test_rhs_hcof_advanced(function_tmpdir): def callback(sim, step): model = sim.test_model if step == Callbacks.timestep_start: @@ -410,7 +413,7 @@ def callback(sim, step): name = "dis_model" sim_pth = data_pth / name - test_pth = tmpdir / name + test_pth = function_tmpdir / name shutil.copytree(sim_pth, test_pth, dirs_exist_ok=True) try: diff --git a/autotest/test_mf6_examples.py b/autotest/test_mf6_examples.py index 8ed392b..36e1647 100644 --- a/autotest/test_mf6_examples.py +++ b/autotest/test_mf6_examples.py @@ -9,14 +9,14 @@ dll = "libmf6" -def test_mf6_example_simulations(tmpdir, mf6_example_namfiles): +def test_mf6_example_simulations(function_tmpdir, mf6_example_namfiles): """ MF6 examples parametrized by simulation. `mf6_example_namfiles` is a list of models to run in order provided. Coupled models share the same tempdir Parameters ---------- - tmpdir: function-scoped temporary directory fixture + function_tmpdir: function-scoped temporary directory fixture mf6_example_namfiles: ordered list of namfiles for 1+ coupled models """ if len(mf6_example_namfiles) == 0: @@ -24,10 +24,11 @@ def test_mf6_example_simulations(tmpdir, mf6_example_namfiles): namfile = Path(mf6_example_namfiles[0]) nested = is_nested(namfile) - tmpdir = Path(tmpdir / "workspace") + function_tmpdir = Path(function_tmpdir / "workspace") copytree( - src=namfile.parent.parent if nested else namfile.parent, dst=tmpdir + src=namfile.parent.parent if nested else namfile.parent, + dst=function_tmpdir, ) def callback(sim, step): @@ -37,12 +38,15 @@ def run_models(): # run models in order received (should be alphabetical, so gwf precedes gwt) for namfile in mf6_example_namfiles: namfile_path = Path(namfile).resolve() - namfile_name = namfile_path.name model_path = namfile_path.parent # working directory must be named according to the name file's parent (e.g. # 'mf6gwf') because coupled models refer to each other with relative paths - wrkdir = Path(tmpdir / model_path.name) if nested else tmpdir + wrkdir = ( + Path(function_tmpdir / model_path.name) + if nested + else function_tmpdir + ) try: run_simulation(dll, wrkdir, callback, verbose=True) except Exception as e: diff --git a/examples/notebooks/Head_Monitor_Example.ipynb b/examples/notebooks/Head_Monitor_Example.ipynb index 91a2219..20ea830 100644 --- a/examples/notebooks/Head_Monitor_Example.ipynb +++ b/examples/notebooks/Head_Monitor_Example.ipynb @@ -17,11 +17,14 @@ "metadata": {}, "outputs": [], "source": [ - "from IPython.display import clear_output, display # remove this import if adapted to python script\n", + "from IPython.display import (\n", + " clear_output,\n", + " display,\n", + ") # remove this import if adapted to python script\n", "\n", "from modflowapi import run_simulation, Callbacks\n", "from flopy.discretization import StructuredGrid\n", - "from flopy.plot import PlotMapView, styles\n", + "from flopy.plot import PlotMapView\n", "from pathlib import Path\n", "import numpy as np\n", "import matplotlib.pyplot as plt" @@ -47,12 +50,12 @@ "class StructuredHeadMonitor:\n", " \"\"\"\n", " An example class that reverses the model gradient by\n", - " swapping CHD boundary conditions each stress period, \n", + " swapping CHD boundary conditions each stress period,\n", " and monitors the head at each timestep by updating\n", - " a matplotlib plot. This class could be adapted to \n", + " a matplotlib plot. This class could be adapted to\n", " be used as a head monitor to observe other changes\n", " in the model by modifying the callback class.\n", - " \n", + "\n", " Parameters\n", " ----------\n", " layer: int\n", @@ -77,7 +80,7 @@ " def build_modelgrid(self, ml):\n", " \"\"\"\n", " Method to update the matplotlib plot\n", - " \n", + "\n", " Parameters\n", " ----------\n", " ml : ApiModel\n", @@ -89,11 +92,7 @@ " botm = ml.dis.bot.values\n", " idomain = ml.dis.idomain.values\n", " self.modelgrid = StructuredGrid(\n", - " delc=delc,\n", - " delr=delr,\n", - " top=top,\n", - " botm=botm,\n", - " idomain=idomain\n", + " delc=delc, delr=delr, top=top, botm=botm, idomain=idomain\n", " )\n", "\n", " def initialize_plot(self):\n", @@ -103,7 +102,9 @@ " fig, ax = plt.subplots(figsize=(8, 8))\n", " self.fig = fig\n", " self.ax = ax\n", - " self.pmv = PlotMapView(modelgrid=self.modelgrid, ax=ax, layer=self.layer)\n", + " self.pmv = PlotMapView(\n", + " modelgrid=self.modelgrid, ax=ax, layer=self.layer\n", + " )\n", " grid = self.pmv.plot_grid()\n", " idm = self.pmv.plot_inactive()\n", " initial = np.full(self.modelgrid.shape, np.nan)\n", @@ -113,7 +114,7 @@ " def update_plot(self, ml):\n", " \"\"\"\n", " Method to update the matplotlib plot\n", - " \n", + "\n", " Parameters\n", " ----------\n", " ml : ApiModel\n", @@ -124,16 +125,16 @@ " grid = self.pmv.plot_grid()\n", " idm = self.pmv.plot_inactive()\n", " self.pc = self.pmv.plot_array(heads, vmin=self.vmin, vmax=self.vmax)\n", - " \n", + "\n", " # only applicable to jupyter notebooks, remove these two lines in python scipt\n", - " display(self.fig) \n", + " display(self.fig)\n", " if ml.kper == (ml.nper - 1) and ml.kstp == (ml.nstp - 1):\n", " pass\n", " else:\n", - " clear_output(wait = True) \n", - " \n", - " # the pause time can be reduced if adapted in python script \n", - " plt.pause(0.1) \n", + " clear_output(wait=True)\n", + "\n", + " # the pause time can be reduced if adapted in python script\n", + " plt.pause(0.1)\n", "\n", " def callback(self, sim, callback_step):\n", " \"\"\"\n", @@ -145,7 +146,7 @@ " Parameters\n", " ----------\n", " sim : modflowapi.Simulation\n", - " A simulation object for the solution group that is \n", + " A simulation object for the solution group that is\n", " currently being solved\n", " callback_step : enumeration\n", " modflowapi.Callbacks enumeration object that indicates\n", @@ -173,7 +174,7 @@ "\n", " if callback_step == Callbacks.timestep_end:\n", " ml = sim.get_model()\n", - " self.update_plot(ml)\n" + " self.update_plot(ml)" ] }, { diff --git a/examples/notebooks/MODFLOW-API_extensions_objects.ipynb b/examples/notebooks/MODFLOW-API_extensions_objects.ipynb index 87cb991..cef3af7 100644 --- a/examples/notebooks/MODFLOW-API_extensions_objects.ipynb +++ b/examples/notebooks/MODFLOW-API_extensions_objects.ipynb @@ -50,7 +50,7 @@ " ext = \".so\"\n", "else:\n", " ext = \".dylib\"\n", - " \n", + "\n", "dll = Path(dll + ext)" ] }, @@ -191,7 +191,7 @@ "metadata": {}, "outputs": [], "source": [ - "model = sim.get_model('test_model')\n", + "model = sim.get_model(\"test_model\")\n", "model" ] }, @@ -368,7 +368,7 @@ "metadata": {}, "outputs": [], "source": [ - "recarray['recharge'][0] *= 100\n", + "recarray[\"recharge\"][0] *= 100\n", "rch.stress_period_data.values = recarray\n", "\n", "# show that values have been updated\n", @@ -472,7 +472,7 @@ "outputs": [], "source": [ "recarray.resize((nbound + 1,), refcheck=False)\n", - "recarray[-1] = ((0, 1, 5), -20., 0, 1)\n", + "recarray[-1] = ((0, 1, 5), -20.0, 0, 1)\n", "recarray" ] }, @@ -754,11 +754,11 @@ " \"\"\"\n", " A demonstration function that dynamically adjusts recharge\n", " and pumping in a modflow-6 model through the MODFLOW-API\n", - " \n", + "\n", " Parameters\n", " ----------\n", " sim : modflowapi.Simulation\n", - " A simulation object for the solution group that is \n", + " A simulation object for the solution group that is\n", " currently being solved\n", " callback_step : enumeration\n", " modflowapi.Callbacks enumeration object that indicates\n", @@ -767,7 +767,7 @@ " ml = sim.test_model\n", " if callback_step == Callbacks.initialize:\n", " print(sim.models)\n", - " \n", + "\n", " if callback_step == Callbacks.stress_period_start:\n", " # adjust recharge for stress periods 1 through 7\n", " if sim.kper <= 6:\n", @@ -775,16 +775,16 @@ " spd = rcha.stress_period_data\n", " print(f\"updating recharge: stress_period={ml.kper}\")\n", " spd[\"recharge\"] += 0.40 * sim.kper\n", - " \n", - " \n", + "\n", " if callback_step == Callbacks.timestep_start:\n", - " print(f\"updating wel flux: stress_period={ml.kper}, timestep={ml.kstp}\")\n", + " print(\n", + " f\"updating wel flux: stress_period={ml.kper}, timestep={ml.kstp}\"\n", + " )\n", " ml.wel.stress_period_data[\"q\"] -= ml.kstp * 1.5\n", - " \n", + "\n", " if callback_step == Callbacks.iteration_start:\n", " # we can implement complex solutions to boundary conditions here!\n", - " pass\n", - " " + " pass" ] }, { diff --git a/examples/notebooks/Quickstart.ipynb b/examples/notebooks/Quickstart.ipynb index 9c6eff7..ddd7ac8 100644 --- a/examples/notebooks/Quickstart.ipynb +++ b/examples/notebooks/Quickstart.ipynb @@ -19,7 +19,6 @@ "source": [ "import modflowapi\n", "from modflowapi import Callbacks\n", - "import numpy as np\n", "from pathlib import Path" ] }, @@ -74,11 +73,11 @@ " \"\"\"\n", " A demonstration function that dynamically adjusts recharge\n", " and pumping in a modflow-6 model through the MODFLOW-API\n", - " \n", + "\n", " Parameters\n", " ----------\n", " sim : modflowapi.ApiSimulation\n", - " A simulation object for the solution group that is \n", + " A simulation object for the solution group that is\n", " currently being solved\n", " step : enumeration\n", " modflowapi.Callbacks enumeration object that indicates\n", @@ -87,7 +86,7 @@ " ml = sim.test_model\n", " if callback_step == Callbacks.initialize:\n", " print(sim.models)\n", - " \n", + "\n", " if callback_step == Callbacks.stress_period_start:\n", " # adjust recharge for stress periods 7 through 12\n", " if sim.kper <= 6:\n", @@ -95,16 +94,16 @@ " spd = rcha.stress_period_data\n", " print(f\"updating recharge: stress_period={ml.kper}\")\n", " spd[\"recharge\"] += 0.40 * sim.kper\n", - " \n", - " \n", + "\n", " if callback_step == Callbacks.timestep_start:\n", - " print(f\"updating wel flux: stress_period={ml.kper}, timestep={ml.kstp}\")\n", + " print(\n", + " f\"updating wel flux: stress_period={ml.kper}, timestep={ml.kstp}\"\n", + " )\n", " ml.wel.stress_period_data[\"q\"] -= ml.kstp * 1.5\n", - " \n", + "\n", " if callback_step == Callbacks.iteration_start:\n", " # we can implement complex solutions to boundary conditions here!\n", - " pass\n", - " " + " pass" ] }, { diff --git a/modflowapi/__init__.py b/modflowapi/__init__.py index dccdd54..0526956 100644 --- a/modflowapi/__init__.py +++ b/modflowapi/__init__.py @@ -1,4 +1,4 @@ -# imports +# ruff: noqa: F401, allow imports directly from modflowapi from .version import __version__ from modflowapi.modflowapi import ModflowApi diff --git a/modflowapi/extensions/__init__.py b/modflowapi/extensions/__init__.py index 9fa55e0..f7cbfa5 100644 --- a/modflowapi/extensions/__init__.py +++ b/modflowapi/extensions/__init__.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401, allow imports directly from modflowapi.extensions from .apisimulation import ApiSimulation from .apimodel import ApiModel from .apiexchange import ApiExchange diff --git a/modflowapi/extensions/apimodel.py b/modflowapi/extensions/apimodel.py index d899d7d..e60e0da 100644 --- a/modflowapi/extensions/apimodel.py +++ b/modflowapi/extensions/apimodel.py @@ -366,8 +366,8 @@ def _set_node_mapping(self): user arrays to modflow's internal arrays """ node_addr = self.mf6.get_var_address("NODES", self.name, self.dis_name) - nodes = self.mf6.get_value(node_addr) - if nodes[0] == self.size: + nodes = self.mf6.get_value(node_addr).item() + if nodes == self.size: nodeuser = np.arange(nodes).astype(int) nodereduced = np.copy(nodeuser) else: diff --git a/modflowapi/extensions/apisimulation.py b/modflowapi/extensions/apisimulation.py index 5d96dde..e71816e 100644 --- a/modflowapi/extensions/apisimulation.py +++ b/modflowapi/extensions/apisimulation.py @@ -52,13 +52,13 @@ def __repr__(self): s += f"Number of models: {len(self._models)}:\n" for name, obj in self._models.items(): s += f"\t{name} : {type(obj)}\n" - s += f"Simulation level packages include:\n" + s += "Simulation level packages include:\n" s += f"\tSLN: {self.sln}\n" s += f"\tTDIS: {self.tdis}\n" if self.ats_active: s += f"\tATS: {self.ats}\n" if self._exchanges: - s += f"\tExchanges include:\n" + s += "\tExchanges include:\n" for name, exchange in self._exchanges.items(): f"\t\t{name}: {type(exchange)}\n" diff --git a/modflowapi/extensions/runner.py b/modflowapi/extensions/runner.py index 11e67fe..dc9e241 100644 --- a/modflowapi/extensions/runner.py +++ b/modflowapi/extensions/runner.py @@ -1,7 +1,5 @@ from .. import ModflowApi from .apisimulation import ApiSimulation -import pathlib -import platform from enum import Enum diff --git a/pyproject.toml b/pyproject.toml index 9136c6e..9bc110c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,10 +48,7 @@ test = [ "pytest-xdist", ] lint = [ - "black", - "flake8", - "isort", - "pylint", + "ruff", ] [project.urls] @@ -64,6 +61,21 @@ version = {attr = "modflowapi.version.__version__"} [tool.setuptools.packages.find] include = ["modflowapi", "modflowapi.*"] -[tool.black] +[tool.ruff] +target-version = "py38" line-length = 79 -target_version = ["py37"] +include = [ + "pyproject.toml", + "modflowapi/**/*.py", + "autotest/**/*.py", + "examples/**/*.py", + "scripts/**/*.py", +] +extend-include = [ + "examples/**/*.ipynb" +] + +[tool.ruff.lint] +ignore = [ + "F841" # local variable assigned but never used +] \ No newline at end of file diff --git a/scripts/update_version.py b/scripts/update_version.py index bf3cc9f..9e90e6b 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -71,7 +71,7 @@ def update_version( finally: try: lock_path.unlink() - except: + except FileNotFoundError: pass