From 5c698a9c2333decf541b28d7221b970f35c72019 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 28 Dec 2022 17:14:39 +0000 Subject: [PATCH 1/6] ci(release): update to development version 0.0.8 --- README.md | 2 +- modflow_devtools/__init__.py | 2 +- version.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a88cfeb..dc263e6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MODFLOW developer tools -### Version 0.0.7 — release candidate +### Version 0.0.8 — release candidate [![CI](https://github.com/MODFLOW-USGS/modflow-devtools/actions/workflows/ci.yml/badge.svg)](https://github.com/MODFLOW-USGS/modflow-devtools/actions/workflows/ci.yml) [![GitHub tag](https://img.shields.io/github/tag/MODFLOW-USGS/modflow-devtools.svg)](https://github.com/MODFLOW-USGS/modflow-devtools/tags/latest) [![PyPI Version](https://img.shields.io/pypi/v/modflow-devtools.png)](https://pypi.python.org/pypi/modflow-devtools) diff --git a/modflow_devtools/__init__.py b/modflow_devtools/__init__.py index 712b74c..7ae4cad 100644 --- a/modflow_devtools/__init__.py +++ b/modflow_devtools/__init__.py @@ -1,6 +1,6 @@ __author__ = "Joseph D. Hughes" __date__ = "Dec 28, 2022" -__version__ = "0.0.7" +__version__ = "0.0.8" __maintainer__ = "Joseph D. Hughes" __email__ = "jdhughes@usgs.gov" __status__ = "Production" diff --git a/version.txt b/version.txt index 5c4511c..7d6b3eb 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.0.7 \ No newline at end of file +0.0.8 \ No newline at end of file From ec64e70cc1066fef373d3fc0fa87fc6e30ac225b Mon Sep 17 00:00:00 2001 From: w-bonelli <wbonelli@ucar.edu> Date: Wed, 28 Dec 2022 12:25:58 -0500 Subject: [PATCH 2/6] Delete CHANGELOG.md --- CHANGELOG.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 208fd71..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -### Version 0.0.7 - -#### Refactor - -* [refactor(executables)](https://github.com/modflowpy/flopy/commit/58c3642d0e6d20d5e34783b5b61e8238058e102f): Simplify exes container, allow dict access (#24). Committed by w-bonelli on 2022-12-28. -* [refactor](https://github.com/modflowpy/flopy/commit/50c83a9eaed532722549a2d9da1eb79ed8cf01be): Drop Python 3.7, add Python 3.11 (#25). Committed by w-bonelli on 2022-12-28. - From b62547bd607f9a0d3a78be61d16976bf406151f5 Mon Sep 17 00:00:00 2001 From: w-bonelli <wbonelli@ucar.edu> Date: Wed, 28 Dec 2022 12:57:08 -0500 Subject: [PATCH 3/6] fix(release): exclude intermediate changelog (#28) --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9378749..65e1a31 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,6 +101,7 @@ jobs: # remove this release's changelog so we don't commit it # the changes have already been prepended to HISTORY.md rm ${{ steps.update-changelog.outputs.changelog }} + rm -f CHANGELOG.md # commit and push changes git config core.sharedRepository true From 81077b87f36fe78ff64747d7b6d49ff8533a16bc Mon Sep 17 00:00:00 2001 From: w-bonelli <wbonelli@ucar.edu> Date: Wed, 28 Dec 2022 17:35:29 -0500 Subject: [PATCH 4/6] docs: set up ReadTheDocs (#29) --- .gitignore | 6 +- .readthedocs.yml | 12 ++ README.md | 311 ++------------------------------------ docs/Makefile | 20 +++ docs/conf.py | 31 ++++ docs/index.rst | 50 ++++++ docs/make.bat | 35 +++++ docs/md/act.md | 21 +++ docs/md/cases.md | 64 ++++++++ docs/md/doctoc.md | 11 ++ docs/md/download.md | 1 + docs/md/executables.md | 31 ++++ docs/md/fixtures.md | 90 +++++++++++ docs/md/install.md | 32 ++++ docs/md/markers.md | 81 ++++++++++ docs/md/zip.md | 3 + scripts/update_version.py | 14 ++ setup.cfg | 4 + 18 files changed, 514 insertions(+), 303 deletions(-) create mode 100644 .readthedocs.yml create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/md/act.md create mode 100644 docs/md/cases.md create mode 100644 docs/md/doctoc.md create mode 100644 docs/md/download.md create mode 100644 docs/md/executables.md create mode 100644 docs/md/fixtures.md create mode 100644 docs/md/install.md create mode 100644 docs/md/markers.md create mode 100644 docs/md/zip.md diff --git a/.gitignore b/.gitignore index 4ec3575..4ad02b9 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,8 @@ modflow_devtools/bin/ modflow_devtools/utilities/temp/ # git-cliff-action likes to add app/ folder to the project root -app \ No newline at end of file +app + +# in case developer installs modflow executables in the project root +bin + diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..99b914b --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,12 @@ +version: 2 +sphinx: + configuration: docs/conf.py +formats: + - pdf +python: + version: "3.8" + install: + - method: pip + path: . + extra_requirements: + - docs \ No newline at end of file diff --git a/README.md b/README.md index dc263e6..46cb456 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # MODFLOW developer tools ### Version 0.0.8 — release candidate -[![CI](https://github.com/MODFLOW-USGS/modflow-devtools/actions/workflows/ci.yml/badge.svg)](https://github.com/MODFLOW-USGS/modflow-devtools/actions/workflows/ci.yml) [![GitHub tag](https://img.shields.io/github/tag/MODFLOW-USGS/modflow-devtools.svg)](https://github.com/MODFLOW-USGS/modflow-devtools/tags/latest) [![PyPI Version](https://img.shields.io/pypi/v/modflow-devtools.png)](https://pypi.python.org/pypi/modflow-devtools) -[![PyPI Status](https://img.shields.io/pypi/status/modflow-devtools.png)](https://pypi.python.org/pypi/modflow-devtools) [![PyPI Versions](https://img.shields.io/pypi/pyversions/modflow-devtools.png)](https://pypi.python.org/pypi/modflow-devtools) +[![PyPI Status](https://img.shields.io/pypi/status/modflow-devtools.png)](https://pypi.python.org/pypi/modflow-devtools) +[![CI](https://github.com/MODFLOW-USGS/modflow-devtools/actions/workflows/ci.yml/badge.svg)](https://github.com/MODFLOW-USGS/modflow-devtools/actions/workflows/ci.yml) +[![Documentation Status](https://readthedocs.org/projects/modflow-devtools/badge/?version=latest)](https://modflow-devtools.readthedocs.io/en/latest/?badge=latest) Python tools for MODFLOW development and testing. @@ -14,20 +15,9 @@ Python tools for MODFLOW development and testing. - [Requirements](#requirements) - [Installation](#installation) -- [Included](#included) - - [`MFZipFile` class](#mfzipfile-class) - - [Keepable temporary directory fixtures](#keepable-temporary-directory-fixtures) - - [Model-loading fixtures](#model-loading-fixtures) - - [Test models](#test-models) - - [Example scenarios](#example-scenarios) - - [Reusable test case framework](#reusable-test-case-framework) - - [Parametrizing with `Case`](#parametrizing-with-case) - - [Generating cases dynamically](#generating-cases-dynamically) - - [Executables container](#executables-container) - - [Conditionally skipping tests](#conditionally-skipping-tests) - - [Miscellaneous](#miscellaneous) - - [Generating TOCs with `doctoc`](#generating-tocs-with-doctoc) - - [Testing CI workflows with `act`](#testing-ci-workflows-with-act) +- [Use cases](#use-cases) +- [Documentation](#documentation) +- [Miscellaneous](#miscellaneous) - [MODFLOW Resources](#modflow-resources) <!-- END doctoc generated TOC please keep comment here to allow auto update --> @@ -46,7 +36,7 @@ pip install modflow-devtools To install from source and set up a development environment please see the [developer documentation](DEVELOPER.md). -## Included +## Use cases This package contains shared tools for developing and testing MODFLOW 6 and FloPy, including standalone utilities as well as `pytest` fixtures, CLI options, and test cases: @@ -68,292 +58,9 @@ pytest_plugins = [ "modflow_devtools.fixtures" ] Note that `pytest` requires this to be a top-level `conftest.py` living in your project root. Nested `conftest.py` files may override or extend this package's behavior. -### `MFZipFile` class - -Python's `ZipFile` doesn't preserve execute permissions. The `MFZipFile` subclass modifies `ZipFile.extract()` to do so, as per the recommendation [here](https://stackoverflow.com/questions/39296101/python-zipfile-removes-execute-permissions-from-binaries). - -### Keepable temporary directory fixtures - -Tests often need to exercise code that reads from and/or writes to disk. The test harness may also need to create test data during setup and clean up the filesystem on teardown. Temporary directories are built into `pytest` via the [`tmp_path`](https://docs.pytest.org/en/latest/how-to/tmp_path.html#the-tmp-path-fixture) and `tmp_path_factory` fixtures. - -Several fixtures are provided in `modflow_devtools/fixtures.py` to extend the behavior of temporary directories for test functions: - -- `function_tmpdir` -- `module_tmpdir` -- `class_tmpdir` -- `session_tmpdir` - -These are automatically created before test code runs and lazily removed afterwards, subject to the same [cleanup procedure](https://docs.pytest.org/en/latest/how-to/tmp_path.html#the-default-base-temporary-directory) used by the default `pytest` temporary directory fixtures. Their purpose is to allow test artifacts to be saved in a user-specified location when `pytest` is invoked with a `--keep` option — this can be useful to debug failing tests. - -```python -from pathlib import Path -import inspect - -def test_tmpdirs(function_tmpdir, module_tmpdir): - # function-scoped temporary directory - assert function_tmpdir.is_dir() - assert inspect.currentframe().f_code.co_name in function_tmpdir.stem - - # module-scoped temp dir (accessible to other tests in the script) - assert module_tmpdir.is_dir() - - with open(function_tmpdir / "test.txt", "w") as f1, open(module_tmpdir / "test.txt", "w") as f2: - f1.write("hello, function") - f2.write("hello, module") -``` - -Any files written to the temporary directory will be saved to saved to subdirectories named according to the test case, class or module. To keep files created by a test case like above, run: - -```shell -pytest --keep <path> -``` - -There is also a `--keep-failed <path>` option which preserves outputs only from failing test cases. - -### Model-loading fixtures - -Fixtures are provided to load models from the MODFLOW 6 example and test model repositories and feed them to test functions. Models can be loaded from: - -- [`MODFLOW-USGS/modflow6-examples`](https://github.com/MODFLOW-USGS/modflow6-examples) -- [`MODFLOW-USGS/modflow6-testmodels`](https://github.com/MODFLOW-USGS/modflow6-testmodels) -- [`MODFLOW-USGS/modflow6-largetestmodels`](https://github.com/MODFLOW-USGS/modflow6-largetestmodels) - -These models can be requested like any other `pytest` fixture, by adding one of the following parameters to test functions: - -- `test_model_mf5to6` -- `test_model_mf6` -- `large_test_model` -- `example_scenario` - -To use these fixtures, the environment variable `REPOS_PATH` must point to the location of model repositories on the filesystem. Model repositories must live side-by-side in this location. If `REPOS_PATH` is not configured, test functions requesting these fixtures will be skipped. - -**Note**: example models must be built by running the `ci_build_files.py` script in `modflow6-examples/etc` before running tests using the `example_scenario` fixture. - -#### Test models - -The `test_model_mf5to6`, `test_model_mf6` and `large_test_model` fixtures are each a `Path` to the directory containing the model's namefile. For instance, to load `mf5to6` models from the [`MODFLOW-USGS/modflow6-testmodels`](https://github.com/MODFLOW-USGS/modflow6-testmodels) repository: - -```python -def test_mf5to6_model(tmpdir, testmodel_mf5to6): - assert testmodel_mf5to6.is_dir() -``` - -This test function will be parametrized with all `mf5to6` models found in the `testmodels` repository (likewise for `mf6` models, and for large test models in their own repository). - -#### Example scenarios - -The [`MODFLOW-USGS/modflow6-examples`](https://github.com/MODFLOW-USGS/modflow6-examples) repository contains a collection of scenarios, each consisting of 1 or more models. The `example_scenario` fixture is a `Tuple[str, List[Path]]`. The first item is the name of the scenario. The second item is a list of namefile `Path`s, ordered alphabetically by name. Model naming conventions are as follows: - -- groundwater flow models begin with prefix `gwf*` -- transport models begin with `gwt*` - -Ordering as above permits models to be run directly in the order provided, with transport models potentially consuming the outputs of flow models. A straightforward pattern is to loop over models and run each in a subdirectory of the same top-level working directory. - -```python -def test_example_scenario(tmpdir, example_scenario): - name, namefiles = example_scenario - for namefile in namefiles: - model_ws = tmpdir / namefile.parent.name - model_ws.mkdir() - # load and run model - # ... -``` - -### Reusable test case framework - -A second approach to testing, more flexible than loading pre-existing models from a repository, is to construct test models in code. This typically involves defining variables or `pytest` fixtures in the same test script as the test function. While this pattern is effective for manually defined scenarios, it tightly couples test functions to test cases, prevents easy reuse of the test case by other tests, and tends to lead to duplication, as each test script may reproduce similar test functions and data-generation procedures. - -This package provides a minimal framework for self-describing test cases which can be defined once and plugged into arbitrary test functions. At its core is the `Case` class, which is just a `SimpleNamespace` with a few defaults and a `copy_update()` method for easy modification. This pairs nicely with [`pytest-cases`](https://smarie.github.io/python-pytest-cases/), which is recommended but not required. - -A `Case` requires only a `name`, and has a single default attribute, `xfail=False`, indicating whether the test case is expected to succeed. (Test functions may of course choose to use or ignore this.) - -#### Parametrizing with `Case` - -`Case` can be used with `@pytest.mark.parametrize()` as usual. For instance: - -```python -import pytest -from modflow_devtools.case import Case - -template = Case(name="QA") -cases = [ - template.copy_update(name=template.name + "1", - question="What's the meaning of life, the universe, and everything?", - answer=42), - template.copy_update(name=template.name + "2", - question="Is a Case immutable?", - answer="No, but it's probably best not to mutate it.") -] - - -@pytest.mark.parametrize("case", cases) -def test_cases(case): - assert len(cases) == 2 - assert cases[0] != cases[1] -``` - -#### Generating cases dynamically - -One pattern possible with `pytest-cases` is to programmatically generate test cases by parametrizing a function. This can be a convenient way to produce several similar test cases from a template: - -```python -from pytest_cases import parametrize, parametrize_with_cases -from modflow_devtools.case import Case - - -template = Case(name="QA") -gen_cases = [template.copy_update(name=f"{template.name}{i}", question=f"Q{i}", answer=f"A{i}") for i in range(3)] -info = "cases can be modified further in the generator function,"\ - " or the function may construct and return another object" - - -@parametrize(case=gen_cases, ids=[c.name for c in gen_cases]) -def qa_cases(case): - return case.copy_update(info=info) - - -@parametrize_with_cases("case", cases=".", prefix="qa_") -def test_qa(case): - assert "QA" in case.name - assert info == case.info - print(f"{case.name}:", f"{case.question}? {case.answer}") - print(case.info) -``` - -### Executables container - -The `Executables` class is just a mapping between executable names and paths on the filesystem. This can be useful to test multiple versions of the same program, and is easily injected into test functions as a fixture: - -```python -from os import environ -from pathlib import Path -import subprocess -import sys - -import pytest - -from modflow_devtools.misc import get_suffixes -from modflow_devtools.executables import Executables - -_bin_path = Path("~/.local/bin/modflow").expanduser() -_dev_path = Path(environ.get("BIN_PATH")).absolute() -_ext, _ = get_suffixes(sys.platform) - -@pytest.fixture -@pytest.mark.skipif(not (_bin_path.is_dir() and _dev_path.is_dir())) -def exes(): - return Executables( - mf6_rel=_bin_path / f"mf6{_ext}", - mf6_dev=_dev_path / f"mf6{_ext}" - ) - -def test_exes(exes): - print(subprocess.check_output([f"{exes.mf6_rel}", "-v"]).decode('utf-8')) - print(subprocess.check_output([f"{exes.mf6_dev}", "-v"]).decode('utf-8')) -``` - -### Conditionally skipping tests - -Several `pytest` markers are provided to conditionally skip tests based on executable availability, Python package environment or operating system. - -To skip tests if one or more executables are not available on the path: - -```python -from shutil import which -from modflow_devtools.markers 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") -``` - -To skip tests if one or more Python packages are not available: - -```python -from modflow_devtools.markers import requires_pkg - -@requires_pkg("pandas") -def test_needs_pandas(): - import pandas as pd - -@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: - -```python -import os -import platform -from modflow_devtools.markers import requires_platform, excludes_platform - -@requires_platform("Windows") -def test_needs_windows(): - assert platform.system() == "Windows" - -@excludes_platform("Darwin", ci_only=True) -def test_breaks_osx_ci(): - if "CI" in os.environ: - assert platform.system() != "Darwin" -``` - -Platforms must be specified as returned by `platform.system()`. - -Both these markers accept a `ci_only` flag, which indicates whether the policy should only apply when the test is running on GitHub Actions CI. - -Markers are also provided to ping network resources and skip if unavailable: - -- `@requires_github`: skips if `github.com` is unreachable -- `@requires_spatial_reference`: skips if `spatialreference.org` is unreachable - -### Miscellaneous - -A few other useful tools for MODFLOW 6 and FloPy development include: - -- [`doctoc`](https://www.npmjs.com/package/doctoc): automatically generate table of contents sections for markdown files -- [`act`](https://github.com/nektos/act): test GitHub Actions workflows locally (requires Docker) - -#### Generating TOCs with `doctoc` - -The [`doctoc`](https://www.npmjs.com/package/doctoc) tool can be used to automatically generate table of contents sections for markdown files. `doctoc` is distributed with the [Node Package Manager](https://docs.npmjs.com/cli/v7/configuring-npm/install). With Node installed use `npm install -g doctoc` to install `doctoc` globally. Then just run `doctoc <file>`, e.g.: - -```shell -doctoc DEVELOPER.md -``` - -This will insert HTML comments surrounding an automatically edited region, scanning for headers and creating an appropriately indented TOC tree. Subsequent runs are idempotent, updating if the file has changed or leaving it untouched if not. - -To run `doctoc` for all markdown files in a particular directory (recursive), use `doctoc some/path`. - -#### Testing CI workflows with `act` - -The [`act`](https://github.com/nektos/act) tool uses Docker to run containerized CI workflows in a simulated GitHub Actions environment. [Docker Desktop](https://www.docker.com/products/docker-desktop/) is required for Mac or Windows and [Docker Engine](https://docs.docker.com/engine/) on Linux. - -With Docker installed and running, run `act -l` from the project root to see available CI workflows. To run all workflows and jobs, just run `act`. To run a particular workflow use `-W`: - -```shell -act -W .github/workflows/commit.yml -``` - -To run a particular job within a workflow, add the `-j` option: - -```shell -act -W .github/workflows/commit.yml -j build -``` - -**Note:** GitHub API rate limits are easy to exceed, especially with job matrices. Authenticated GitHub users have a much higher rate limit: use `-s GITHUB_TOKEN=<your token>` when invoking `act` to provide a personal access token. Note that this will log your token in shell history — leave the value blank for a prompt to enter it more securely. - -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. +## Documentation +Usage documentation is available at [modflow-devtools.readthedocs.io](https://modflow-devtools.readthedocs.io/en/latest/). ## MODFLOW Resources diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..c1738d9 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,31 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'modflow-devtools' +author = 'MODFLOW Team' +release = '0.0.8' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ['myst_parser'] +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown' +} +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_title = 'MODFLOW Devtools' +html_static_path = ['_static'] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..91030de --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,50 @@ +.. modflow-devtools documentation master file, created by + sphinx-quickstart on Wed Dec 28 16:23:25 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +MODFLOW development tools +================================================ + +The `modflow-devtools` package provides a set of tools for developing and testing MODFLOW 6, FloPy, and related applications. + +.. toctree:: + :maxdepth: 2 + :caption: Introduction + + md/install.md + + +.. toctree:: + :maxdepth: 2 + :caption: Test fixtures + + md/cases.md + md/executables.md + md/fixtures.md + md/markers.md + + +.. toctree:: + :maxdepth: 2 + :caption: Miscellaneous + + md/download.md + md/zip.md + + +.. toctree:: + :maxdepth: 2 + :caption: External tools + + md/act.md + md/doctoc.md + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/md/act.md b/docs/md/act.md new file mode 100644 index 0000000..7a8ad91 --- /dev/null +++ b/docs/md/act.md @@ -0,0 +1,21 @@ +# Local CI testing + +The [`act`](https://github.com/nektos/act) tool uses Docker to run containerized CI workflows in a simulated GitHub Actions environment. [Docker Desktop](https://www.docker.com/products/docker-desktop/) is required for Mac or Windows and [Docker Engine](https://docs.docker.com/engine/) on Linux. + +With Docker installed and running, run `act -l` from the project root to see available CI workflows. To run all workflows and jobs, just run `act`. To run a particular workflow use `-W`: + +```shell +act -W .github/workflows/commit.yml +``` + +To run a particular job within a workflow, add the `-j` option: + +```shell +act -W .github/workflows/commit.yml -j build +``` + +**Note:** GitHub API rate limits are easy to exceed, especially with job matrices. Authenticated GitHub users have a much higher rate limit: use `-s GITHUB_TOKEN=<your token>` when invoking `act` to provide a personal access token. Note that this will log your token in shell history — leave the value blank for a prompt to enter it more securely. + +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 diff --git a/docs/md/cases.md b/docs/md/cases.md new file mode 100644 index 0000000..57c7ee3 --- /dev/null +++ b/docs/md/cases.md @@ -0,0 +1,64 @@ +# Cases + +An alternative approach to testing, rather than loading pre-existing models from a repository, is to construct test models in code. This typically involves defining variables or `pytest` fixtures in the same test script as the test function. While this pattern is effective for manually defined scenarios, it tightly couples test functions to test cases, prevents easy reuse of the test case by other tests, and tends to lead to duplication, as each test script may reproduce similar test functions and data-generation procedures. + +This package provides a minimal framework for self-describing test cases which can be defined once and plugged into arbitrary test functions. At its core is the `Case` class, which is just a `SimpleNamespace` with a few defaults and a `copy_update()` method for easy modification. This pairs nicely with [`pytest-cases`](https://smarie.github.io/python-pytest-cases/), which is recommended but not required. + +## Overview + +A `Case` requires only a `name`, and has a single default attribute, `xfail=False`, indicating whether the test case is expected to succeed. (Test functions may of course choose to use or ignore this.) + +## Usage + +### Parametrizing with `Case` + +`Case` can be used with `@pytest.mark.parametrize()` as usual. For instance: + +```python +import pytest +from modflow_devtools.case import Case + +template = Case(name="QA") +cases = [ + template.copy_update(name=template.name + "1", + question="What's the meaning of life, the universe, and everything?", + answer=42), + template.copy_update(name=template.name + "2", + question="Is a Case immutable?", + answer="No, but it's probably best not to mutate it.") +] + + +@pytest.mark.parametrize("case", cases) +def test_cases(case): + assert len(cases) == 2 + assert cases[0] != cases[1] +``` + +### Generating cases dynamically + +One pattern possible with `pytest-cases` is to programmatically generate test cases by parametrizing a function. This can be a convenient way to produce several similar test cases from a template: + +```python +from pytest_cases import parametrize, parametrize_with_cases +from modflow_devtools.case import Case + + +template = Case(name="QA") +gen_cases = [template.copy_update(name=f"{template.name}{i}", question=f"Q{i}", answer=f"A{i}") for i in range(3)] +info = "cases can be modified further in the generator function,"\ + " or the function may construct and return another object" + + +@parametrize(case=gen_cases, ids=[c.name for c in gen_cases]) +def qa_cases(case): + return case.copy_update(info=info) + + +@parametrize_with_cases("case", cases=".", prefix="qa_") +def test_qa(case): + assert "QA" in case.name + assert info == case.info + print(f"{case.name}:", f"{case.question}? {case.answer}") + print(case.info) +``` \ No newline at end of file diff --git a/docs/md/doctoc.md b/docs/md/doctoc.md new file mode 100644 index 0000000..927368d --- /dev/null +++ b/docs/md/doctoc.md @@ -0,0 +1,11 @@ +# Generating TOCs + +The [`doctoc`](https://www.npmjs.com/package/doctoc) tool can be used to automatically generate table of contents sections for markdown files. `doctoc` is distributed with the [Node Package Manager](https://docs.npmjs.com/cli/v7/configuring-npm/install). With Node installed use `npm install -g doctoc` to install `doctoc` globally. Then just run `doctoc <file>`, e.g.: + +```shell +doctoc DEVELOPER.md +``` + +This will insert HTML comments surrounding an automatically edited region, scanning for headers and creating an appropriately indented TOC tree. Subsequent runs are idempotent, updating if the file has changed or leaving it untouched if not. + +To run `doctoc` for all markdown files in a particular directory (recursive), use `doctoc some/path`. \ No newline at end of file diff --git a/docs/md/download.md b/docs/md/download.md new file mode 100644 index 0000000..d471231 --- /dev/null +++ b/docs/md/download.md @@ -0,0 +1 @@ +# Downloads \ No newline at end of file diff --git a/docs/md/executables.md b/docs/md/executables.md new file mode 100644 index 0000000..b2b02e2 --- /dev/null +++ b/docs/md/executables.md @@ -0,0 +1,31 @@ +# Executables + +The `Executables` class is just a mapping between executable names and paths on the filesystem. This can be useful to test multiple versions of the same program, and is easily injected into test functions as a fixture: + +```python +from os import environ +from pathlib import Path +import subprocess +import sys + +import pytest + +from modflow_devtools.misc import get_suffixes +from modflow_devtools.executables import Executables + +_bin_path = Path("~/.local/bin/modflow").expanduser() +_dev_path = Path(environ.get("BIN_PATH")).absolute() +_ext, _ = get_suffixes(sys.platform) + +@pytest.fixture +@pytest.mark.skipif(not (_bin_path.is_dir() and _dev_path.is_dir())) +def exes(): + return Executables( + mf6_rel=_bin_path / f"mf6{_ext}", + mf6_dev=_dev_path / f"mf6{_ext}" + ) + +def test_exes(exes): + print(subprocess.check_output([f"{exes.mf6_rel}", "-v"]).decode('utf-8')) + print(subprocess.check_output([f"{exes.mf6_dev}", "-v"]).decode('utf-8')) +``` \ No newline at end of file diff --git a/docs/md/fixtures.md b/docs/md/fixtures.md new file mode 100644 index 0000000..4d4e14f --- /dev/null +++ b/docs/md/fixtures.md @@ -0,0 +1,90 @@ +# Fixtures + +Several `pytest` fixtures are provided to help with testing. + +## Keepable temporary directory fixtures + +Tests often need to exercise code that reads from and/or writes to disk. The test harness may also need to create test data during setup and clean up the filesystem on teardown. Temporary directories are built into `pytest` via the [`tmp_path`](https://docs.pytest.org/en/latest/how-to/tmp_path.html#the-tmp-path-fixture) and `tmp_path_factory` fixtures. + +Several fixtures are provided in `modflow_devtools/fixtures.py` to extend the behavior of temporary directories for test functions: + +- `function_tmpdir` +- `module_tmpdir` +- `class_tmpdir` +- `session_tmpdir` + +These are automatically created before test code runs and lazily removed afterwards, subject to the same [cleanup procedure](https://docs.pytest.org/en/latest/how-to/tmp_path.html#the-default-base-temporary-directory) used by the default `pytest` temporary directory fixtures. Their purpose is to allow test artifacts to be saved in a user-specified location when `pytest` is invoked with a `--keep` option — this can be useful to debug failing tests. + +```python +from pathlib import Path +import inspect + +def test_tmpdirs(function_tmpdir, module_tmpdir): + # function-scoped temporary directory + assert function_tmpdir.is_dir() + assert inspect.currentframe().f_code.co_name in function_tmpdir.stem + + # module-scoped temp dir (accessible to other tests in the script) + assert module_tmpdir.is_dir() + + with open(function_tmpdir / "test.txt", "w") as f1, open(module_tmpdir / "test.txt", "w") as f2: + f1.write("hello, function") + f2.write("hello, module") +``` + +Any files written to the temporary directory will be saved to saved to subdirectories named according to the test case, class or module. To keep files created by a test case like above, run: + +```shell +pytest --keep <path> +``` + +There is also a `--keep-failed <path>` option which preserves outputs only from failing test cases. + +## Model-loading fixtures + +Fixtures are provided to load models from the MODFLOW 6 example and test model repositories and feed them to test functions. Models can be loaded from: + +- [`MODFLOW-USGS/modflow6-examples`](https://github.com/MODFLOW-USGS/modflow6-examples) +- [`MODFLOW-USGS/modflow6-testmodels`](https://github.com/MODFLOW-USGS/modflow6-testmodels) +- [`MODFLOW-USGS/modflow6-largetestmodels`](https://github.com/MODFLOW-USGS/modflow6-largetestmodels) + +These models can be requested like any other `pytest` fixture, by adding one of the following parameters to test functions: + +- `test_model_mf5to6` +- `test_model_mf6` +- `large_test_model` +- `example_scenario` + +To use these fixtures, the environment variable `REPOS_PATH` must point to the location of model repositories on the filesystem. Model repositories must live side-by-side in this location. If `REPOS_PATH` is not configured, test functions requesting these fixtures will be skipped. + +**Note**: example models must be built by running the `ci_build_files.py` script in `modflow6-examples/etc` before running tests using the `example_scenario` fixture. + +### Test models + +The `test_model_mf5to6`, `test_model_mf6` and `large_test_model` fixtures are each a `Path` to the directory containing the model's namefile. For instance, to load `mf5to6` models from the [`MODFLOW-USGS/modflow6-testmodels`](https://github.com/MODFLOW-USGS/modflow6-testmodels) repository: + +```python +def test_mf5to6_model(tmpdir, testmodel_mf5to6): + assert testmodel_mf5to6.is_dir() +``` + +This test function will be parametrized with all `mf5to6` models found in the `testmodels` repository (likewise for `mf6` models, and for large test models in their own repository). + +### Example scenarios + +The [`MODFLOW-USGS/modflow6-examples`](https://github.com/MODFLOW-USGS/modflow6-examples) repository contains a collection of scenarios, each consisting of 1 or more models. The `example_scenario` fixture is a `Tuple[str, List[Path]]`. The first item is the name of the scenario. The second item is a list of namefile `Path`s, ordered alphabetically by name. Model naming conventions are as follows: + +- groundwater flow models begin with prefix `gwf*` +- transport models begin with `gwt*` + +Ordering as above permits models to be run directly in the order provided, with transport models potentially consuming the outputs of flow models. A straightforward pattern is to loop over models and run each in a subdirectory of the same top-level working directory. + +```python +def test_example_scenario(tmpdir, example_scenario): + name, namefiles = example_scenario + for namefile in namefiles: + model_ws = tmpdir / namefile.parent.name + model_ws.mkdir() + # load and run model + # ... +``` \ No newline at end of file diff --git a/docs/md/install.md b/docs/md/install.md new file mode 100644 index 0000000..abbe981 --- /dev/null +++ b/docs/md/install.md @@ -0,0 +1,32 @@ +# Installing `modflow-devtools` + +## Official package + +The `modflow-devtools` package is [available on PyPi](https://pypi.org/project/modflow-devtools/) and can be installed with `pip`: + +```shell +pip install modflow-devtools +``` + +## Development version + +To set up a `modflow-devtools` development environment, first clone the repository: + +```shell +git clone https://github.com/MODFLOW-USGS/modflow-devtools.git +``` + +Then install the local copy as well as testing, linting, and docs dependencies: + +``` +pip install . +pip install ".[lint, test, docs]" +``` + +## Using `modflow-devtools` as a `pytest` plugin + +Fixtures provided by `modflow-devtools` can be imported into a `pytest` test suite by adding the following to the consuming project's top-level `conftest.py` file: + +```python +pytest_plugins = ["modflow_devtools"] +``` \ No newline at end of file diff --git a/docs/md/markers.md b/docs/md/markers.md new file mode 100644 index 0000000..bd9714c --- /dev/null +++ b/docs/md/markers.md @@ -0,0 +1,81 @@ +# Markers + +Some broadly useful `pytest` markers are provided. + +## Default markers + +By default, the following markers are defined for any project consuming `modflow-devtools` as a `pytest` plugin: + +- `slow`: tests taking more than a few seconds to complete +- `regression`: tests comparing results from different versions of a program + +### Smoke testing + +[Smoke testing](https://en.wikipedia.org/wiki/Smoke_testing_(software)) is a form of integration testing which aims to exercise a substantial subset of the codebase quickly enough to run often during development. This is useful to rapidly determine whether a refactor has broken any expectations before running slower, more extensive tests. + +To run smoke tests, use the `--smoke` (short `-S`) CLI option. For instance: + +```shell +pytest -v -S +``` + +## Conditionally skipping tests + +Several `pytest` markers are provided to conditionally skip tests based on executable availability, Python package environment or operating system. + +To skip tests if one or more executables are not available on the path: + +```python +from shutil import which +from modflow_devtools.markers 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") +``` + +To skip tests if one or more Python packages are not available: + +```python +from modflow_devtools.markers import requires_pkg + +@requires_pkg("pandas") +def test_needs_pandas(): + import pandas as pd + +@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: + +```python +import os +import platform +from modflow_devtools.markers import requires_platform, excludes_platform + +@requires_platform("Windows") +def test_needs_windows(): + assert platform.system() == "Windows" + +@excludes_platform("Darwin", ci_only=True) +def test_breaks_osx_ci(): + if "CI" in os.environ: + assert platform.system() != "Darwin" +``` + +Platforms must be specified as returned by `platform.system()`. + +Both these markers accept a `ci_only` flag, which indicates whether the policy should only apply when the test is running on GitHub Actions CI. + +Markers are also provided to ping network resources and skip if unavailable: + +- `@requires_github`: skips if `github.com` is unreachable +- `@requires_spatial_reference`: skips if `spatialreference.org` is unreachable \ No newline at end of file diff --git a/docs/md/zip.md b/docs/md/zip.md new file mode 100644 index 0000000..92900dd --- /dev/null +++ b/docs/md/zip.md @@ -0,0 +1,3 @@ +# `MFZipFile` + +Python's `ZipFile` doesn't preserve execute permissions. The `MFZipFile` subclass modifies `ZipFile.extract()` to do so, as per the recommendation [here](https://stackoverflow.com/questions/39296101/python-zipfile-removes-execute-permissions-from-binaries). \ No newline at end of file diff --git a/scripts/update_version.py b/scripts/update_version.py index 00547b3..a22622c 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -13,6 +13,7 @@ _version_txt_path = _project_root_path / "version.txt" _package_init_path = _project_root_path / "modflow_devtools" / "__init__.py" _readme_path = _project_root_path / "README.md" +_docs_config_path = _project_root_path / "docs" / "conf.py" class Version(NamedTuple): @@ -107,6 +108,18 @@ def update_readme_markdown( print(f"Updated {_readme_path} to version {version}") +def update_docs_config( + release_type: ReleaseType, timestamp: datetime, version: Version +): + lines = _docs_config_path.read_text().rstrip().split("\n") + with open(_docs_config_path, "w") as f: + for line in lines: + line = f"release = {version}" if "release = " in line else line + f.write(f"{line}\n") + + print(f"Updated {_docs_config_path} to version {version}") + + def update_version( release_type: ReleaseType, timestamp: datetime = datetime.now(), @@ -126,6 +139,7 @@ def update_version( update_version_txt(release_type, timestamp, version) update_init_py(release_type, timestamp, version) update_readme_markdown(release_type, timestamp, version) + update_docs_config(release_type, timestamp, version) finally: try: lock_path.unlink() diff --git a/setup.cfg b/setup.cfg index 52bb302..432cd06 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,10 @@ test = pytest-dotenv pytest-xdist PyYaml +docs = + sphinx + sphinx-rtd-theme + myst-parser [flake8] From a2d4b9210db532f12cf87ae5d26582d1ed446463 Mon Sep 17 00:00:00 2001 From: w-bonelli <wbonelli@ucar.edu> Date: Wed, 28 Dec 2022 19:23:10 -0500 Subject: [PATCH 5/6] fix(fixtures): fix example_scenario fixture loading (#30) --- README.md | 1 - docs/md/fixtures.md | 8 ++++---- modflow_devtools/fixtures.py | 26 +++++++------------------- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 46cb456..b47c89c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ Python tools for MODFLOW development and testing. - [Installation](#installation) - [Use cases](#use-cases) - [Documentation](#documentation) -- [Miscellaneous](#miscellaneous) - [MODFLOW Resources](#modflow-resources) <!-- END doctoc generated TOC please keep comment here to allow auto update --> diff --git a/docs/md/fixtures.md b/docs/md/fixtures.md index 4d4e14f..961725a 100644 --- a/docs/md/fixtures.md +++ b/docs/md/fixtures.md @@ -55,7 +55,7 @@ These models can be requested like any other `pytest` fixture, by adding one of - `large_test_model` - `example_scenario` -To use these fixtures, the environment variable `REPOS_PATH` must point to the location of model repositories on the filesystem. Model repositories must live side-by-side in this location. If `REPOS_PATH` is not configured, test functions requesting these fixtures will be skipped. +To use these fixtures, the environment variable `REPOS_PATH` must point to the location of model repositories on the filesystem. Model repositories must live side-by-side in this location, and repository directories are expected to be named identically to GitHub repositories. If `REPOS_PATH` is not configured, test functions requesting these fixtures will be skipped. **Note**: example models must be built by running the `ci_build_files.py` script in `modflow6-examples/etc` before running tests using the `example_scenario` fixture. @@ -64,7 +64,7 @@ To use these fixtures, the environment variable `REPOS_PATH` must point to the l The `test_model_mf5to6`, `test_model_mf6` and `large_test_model` fixtures are each a `Path` to the directory containing the model's namefile. For instance, to load `mf5to6` models from the [`MODFLOW-USGS/modflow6-testmodels`](https://github.com/MODFLOW-USGS/modflow6-testmodels) repository: ```python -def test_mf5to6_model(tmpdir, testmodel_mf5to6): +def test_mf5to6_model(testmodel_mf5to6): assert testmodel_mf5to6.is_dir() ``` @@ -80,10 +80,10 @@ The [`MODFLOW-USGS/modflow6-examples`](https://github.com/MODFLOW-USGS/modflow6- Ordering as above permits models to be run directly in the order provided, with transport models potentially consuming the outputs of flow models. A straightforward pattern is to loop over models and run each in a subdirectory of the same top-level working directory. ```python -def test_example_scenario(tmpdir, example_scenario): +def test_example_scenario(tmp_path, example_scenario): name, namefiles = example_scenario for namefile in namefiles: - model_ws = tmpdir / namefile.parent.name + model_ws = tmp_path / namefile.parent.name model_ws.mkdir() # load and run model # ... diff --git a/modflow_devtools/fixtures.py b/modflow_devtools/fixtures.py index e0d47a9..5efdccc 100644 --- a/modflow_devtools/fixtures.py +++ b/modflow_devtools/fixtures.py @@ -221,27 +221,25 @@ def pytest_generate_tests(metafunc): key = "example_scenario" if key in metafunc.fixturenames: - def example_namfile_is_nested(namfile_path: PathLike) -> bool: + def is_nested(namfile_path: PathLike) -> bool: p = Path(namfile_path) if not p.is_file() or not p.name.endswith(".nam"): raise ValueError(f"Expected namefile path, got {p}") - return p.parent.parent.name == "examples" + return p.parent.parent.name != "examples" - def example_name_from_namfile_path(path: PathLike) -> str: + def example_path_from_namfile_path(path: PathLike) -> Path: p = Path(path) if not p.is_file() or not p.name.endswith(".nam"): raise ValueError(f"Expected namefile path, got {p}") - return ( - p.parent.parent.name - if example_namfile_is_nested(p) - else p.parent.name - ) + return p.parent.parent if is_nested(p) else p.parent + + def example_name_from_namfile_path(path: PathLike) -> str: + return example_path_from_namfile_path(path).name def group_examples(namefile_paths) -> Dict[str, List[Path]]: d = OrderedDict() - for name, paths in groupby( namefile_paths, key=example_name_from_namfile_path ): @@ -257,11 +255,6 @@ def group_examples(namefile_paths) -> Dict[str, List[Path]]: return d def get_examples(): - examples_excluded = [ - "ex-gwf-csub-p02c", - "ex-gwt-gwtgwt-mt3dms-p10", - ] - # find and filter namfiles namfiles = [ p @@ -269,11 +262,6 @@ def get_examples(): Path(repos_path) / "modflow6-examples" / "examples" ).rglob("mfsim.nam") ] - namfiles = [ - p - for p in namfiles - if (not any(e in str(p) for e in examples_excluded)) - ] # group example scenarios with multiple models examples = group_examples(namfiles) From bfb6c9269024726c2eac2e76ca63ee131c9557eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Dec 2022 00:25:29 +0000 Subject: [PATCH 6/6] ci(release): set version to 0.0.8, update changelog --- HISTORY.md | 7 +++++++ docs/conf.py | 2 +- modflow_devtools/__init__.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index db06367..07c9793 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,10 @@ +### Version 0.0.8 + +#### Bug fixes + +* [fix(release)](https://github.com/modflowpy/flopy/commit/b62547bd607f9a0d3a78be61d16976bf406151f5): Exclude intermediate changelog (#28). Committed by w-bonelli on 2022-12-28. +* [fix(fixtures)](https://github.com/modflowpy/flopy/commit/a2d4b9210db532f12cf87ae5d26582d1ed446463): Fix example_scenario fixture loading (#30). Committed by w-bonelli on 2022-12-29. + ### Version 0.0.7 #### Refactoring diff --git a/docs/conf.py b/docs/conf.py index c1738d9..e788289 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,7 +8,7 @@ project = 'modflow-devtools' author = 'MODFLOW Team' -release = '0.0.8' +release = 0.0.8 # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/modflow_devtools/__init__.py b/modflow_devtools/__init__.py index 7ae4cad..415edb2 100644 --- a/modflow_devtools/__init__.py +++ b/modflow_devtools/__init__.py @@ -1,5 +1,5 @@ __author__ = "Joseph D. Hughes" -__date__ = "Dec 28, 2022" +__date__ = "Dec 29, 2022" __version__ = "0.0.8" __maintainer__ = "Joseph D. Hughes" __email__ = "jdhughes@usgs.gov"