modflow-devtools 1.4.0__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modflow-devtools-1.4.0/modflow_devtools.egg-info → modflow_devtools-1.6.0}/PKG-INFO +8 -9
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/README.md +3 -2
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/__init__.py +2 -2
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/download.py +28 -39
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/fixtures.py +28 -55
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/imports.py +2 -4
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/latex.py +56 -12
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/markers.py +13 -10
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/misc.py +101 -61
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/ostags.py +2 -2
- modflow_devtools-1.6.0/modflow_devtools/snapshots.py +159 -0
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0/modflow_devtools.egg-info}/PKG-INFO +8 -9
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/SOURCES.txt +1 -0
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/requires.txt +4 -6
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/pyproject.toml +17 -19
- modflow_devtools-1.6.0/version.txt +1 -0
- modflow-devtools-1.4.0/version.txt +0 -1
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/LICENSE.md +0 -0
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/MANIFEST.in +0 -0
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/build.py +0 -0
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/zip.py +0 -0
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/dependency_links.txt +0 -0
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/not-zip-safe +0 -0
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/top_level.txt +0 -0
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/setup.cfg +0 -0
- {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: modflow-devtools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Python tools for MODFLOW development
|
|
5
5
|
Author-email: "Joseph D. Hughes" <modflow@usgs.gov>, Michael Reno <mreno@ucar.edu>, Mike Taves <mwtoews@gmail.com>, Wes Bonelli <wbonelli@ucar.edu>
|
|
6
6
|
Maintainer-email: "Joseph D. Hughes" <modflow@usgs.gov>
|
|
@@ -24,11 +24,7 @@ Requires-Python: >=3.8
|
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE.md
|
|
26
26
|
Provides-Extra: lint
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist: cffconvert; extra == "lint"
|
|
29
|
-
Requires-Dist: flake8; extra == "lint"
|
|
30
|
-
Requires-Dist: isort; extra == "lint"
|
|
31
|
-
Requires-Dist: pylint; extra == "lint"
|
|
27
|
+
Requires-Dist: ruff; extra == "lint"
|
|
32
28
|
Provides-Extra: test
|
|
33
29
|
Requires-Dist: modflow-devtools[lint]; extra == "test"
|
|
34
30
|
Requires-Dist: coverage; extra == "test"
|
|
@@ -37,11 +33,13 @@ Requires-Dist: filelock; extra == "test"
|
|
|
37
33
|
Requires-Dist: meson!=0.63.0; extra == "test"
|
|
38
34
|
Requires-Dist: ninja; extra == "test"
|
|
39
35
|
Requires-Dist: numpy; extra == "test"
|
|
40
|
-
Requires-Dist:
|
|
36
|
+
Requires-Dist: pandas; extra == "test"
|
|
37
|
+
Requires-Dist: pytest!=8.1.0; extra == "test"
|
|
41
38
|
Requires-Dist: pytest-cov; extra == "test"
|
|
42
39
|
Requires-Dist: pytest-dotenv; extra == "test"
|
|
43
40
|
Requires-Dist: pytest-xdist; extra == "test"
|
|
44
41
|
Requires-Dist: PyYaml; extra == "test"
|
|
42
|
+
Requires-Dist: syrupy; extra == "test"
|
|
45
43
|
Provides-Extra: docs
|
|
46
44
|
Requires-Dist: sphinx; extra == "docs"
|
|
47
45
|
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
@@ -105,6 +103,7 @@ Python3.8+, dependency-free, but pairs well with `pytest` and select plugins, e.
|
|
|
105
103
|
|
|
106
104
|
- [`pytest-dotenv`](https://github.com/quiqua/pytest-dotenv)
|
|
107
105
|
- [`pytest-xdist`](https://github.com/pytest-dev/pytest-xdist)
|
|
106
|
+
- [`syrupy`](https://github.com/tophat/syrupy)
|
|
108
107
|
|
|
109
108
|
## Installation
|
|
110
109
|
|
|
@@ -114,7 +113,7 @@ Python3.8+, dependency-free, but pairs well with `pytest` and select plugins, e.
|
|
|
114
113
|
pip install modflow-devtools
|
|
115
114
|
```
|
|
116
115
|
|
|
117
|
-
Pytest, pytest plugins, and other
|
|
116
|
+
Pytest, pytest plugins, and other testing-related dependencies can be installed with:
|
|
118
117
|
|
|
119
118
|
```shell
|
|
120
119
|
pip install "modflow-devtools[test]"
|
|
@@ -122,7 +121,7 @@ pip install "modflow-devtools[test]"
|
|
|
122
121
|
|
|
123
122
|
To install from source and set up a development environment please see the [developer documentation](DEVELOPER.md).
|
|
124
123
|
|
|
125
|
-
To import `pytest` fixtures in a project consuming `modflow-devtools`, add the following to a `conftest.py` file:
|
|
124
|
+
To import `pytest` fixtures in a project consuming `modflow-devtools`, add the following to a test file or `conftest.py` file:
|
|
126
125
|
|
|
127
126
|
```python
|
|
128
127
|
pytest_plugins = [ "modflow_devtools.fixtures" ]
|
|
@@ -56,6 +56,7 @@ Python3.8+, dependency-free, but pairs well with `pytest` and select plugins, e.
|
|
|
56
56
|
|
|
57
57
|
- [`pytest-dotenv`](https://github.com/quiqua/pytest-dotenv)
|
|
58
58
|
- [`pytest-xdist`](https://github.com/pytest-dev/pytest-xdist)
|
|
59
|
+
- [`syrupy`](https://github.com/tophat/syrupy)
|
|
59
60
|
|
|
60
61
|
## Installation
|
|
61
62
|
|
|
@@ -65,7 +66,7 @@ Python3.8+, dependency-free, but pairs well with `pytest` and select plugins, e.
|
|
|
65
66
|
pip install modflow-devtools
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
Pytest, pytest plugins, and other
|
|
69
|
+
Pytest, pytest plugins, and other testing-related dependencies can be installed with:
|
|
69
70
|
|
|
70
71
|
```shell
|
|
71
72
|
pip install "modflow-devtools[test]"
|
|
@@ -73,7 +74,7 @@ pip install "modflow-devtools[test]"
|
|
|
73
74
|
|
|
74
75
|
To install from source and set up a development environment please see the [developer documentation](DEVELOPER.md).
|
|
75
76
|
|
|
76
|
-
To import `pytest` fixtures in a project consuming `modflow-devtools`, add the following to a `conftest.py` file:
|
|
77
|
+
To import `pytest` fixtures in a project consuming `modflow-devtools`, add the following to a test file or `conftest.py` file:
|
|
77
78
|
|
|
78
79
|
```python
|
|
79
80
|
pytest_plugins = [ "modflow_devtools.fixtures" ]
|
|
@@ -58,10 +58,10 @@ def get_releases(
|
|
|
58
58
|
"""
|
|
59
59
|
|
|
60
60
|
if "/" not in repo:
|
|
61
|
-
raise ValueError(
|
|
61
|
+
raise ValueError("repo format must be owner/name")
|
|
62
62
|
|
|
63
63
|
if not isinstance(retries, int) or retries < 1:
|
|
64
|
-
raise ValueError(
|
|
64
|
+
raise ValueError("retries must be a positive int")
|
|
65
65
|
|
|
66
66
|
params = {}
|
|
67
67
|
if per_page is not None:
|
|
@@ -81,7 +81,10 @@ def get_releases(
|
|
|
81
81
|
try:
|
|
82
82
|
if verbose:
|
|
83
83
|
print(
|
|
84
|
-
f"Fetching releases for
|
|
84
|
+
f"Fetching releases for "
|
|
85
|
+
f"repo {repo} "
|
|
86
|
+
f"(page {page}, "
|
|
87
|
+
f"{per_page} per page)"
|
|
85
88
|
)
|
|
86
89
|
with urllib.request.urlopen(request, timeout=10) as resp:
|
|
87
90
|
return json.loads(resp.read().decode())
|
|
@@ -96,9 +99,7 @@ def get_releases(
|
|
|
96
99
|
# GitHub sometimes returns this error for valid URLs, so retry
|
|
97
100
|
warn(f"URL request try {tries} failed ({err})")
|
|
98
101
|
continue
|
|
99
|
-
raise RuntimeError(
|
|
100
|
-
f"cannot retrieve data from {req_url}"
|
|
101
|
-
) from err
|
|
102
|
+
raise RuntimeError(f"cannot retrieve data from {req_url}") from err
|
|
102
103
|
|
|
103
104
|
releases = []
|
|
104
105
|
max_pages = max_pages if max_pages else sys.maxsize
|
|
@@ -132,13 +133,13 @@ def get_release(repo, tag="latest", retries=3, verbose=False) -> dict:
|
|
|
132
133
|
"""
|
|
133
134
|
|
|
134
135
|
if "/" not in repo:
|
|
135
|
-
raise ValueError(
|
|
136
|
+
raise ValueError("repo format must be owner/name")
|
|
136
137
|
|
|
137
138
|
if not isinstance(tag, str) or not any(tag):
|
|
138
|
-
raise ValueError(
|
|
139
|
+
raise ValueError("tag must be a non-empty string")
|
|
139
140
|
|
|
140
141
|
if not isinstance(retries, int) or retries < 1:
|
|
141
|
-
raise ValueError(
|
|
142
|
+
raise ValueError("retries must be a positive int")
|
|
142
143
|
|
|
143
144
|
req_url = f"https://api.github.com/repos/{repo}"
|
|
144
145
|
req_url = (
|
|
@@ -209,10 +210,10 @@ def get_latest_version(repo, retries=3, verbose=False) -> str:
|
|
|
209
210
|
"""
|
|
210
211
|
|
|
211
212
|
if "/" not in repo:
|
|
212
|
-
raise ValueError(
|
|
213
|
+
raise ValueError("repo format must be owner/name")
|
|
213
214
|
|
|
214
215
|
if not isinstance(retries, int) or retries < 1:
|
|
215
|
-
raise ValueError(
|
|
216
|
+
raise ValueError("retries must be a positive int")
|
|
216
217
|
|
|
217
218
|
release = get_release(repo, retries=retries, verbose=verbose)
|
|
218
219
|
return release["tag_name"]
|
|
@@ -245,13 +246,13 @@ def get_release_assets(
|
|
|
245
246
|
"""
|
|
246
247
|
|
|
247
248
|
if "/" not in repo:
|
|
248
|
-
raise ValueError(
|
|
249
|
+
raise ValueError("repo format must be owner/name")
|
|
249
250
|
|
|
250
251
|
if not isinstance(tag, str) or not any(tag):
|
|
251
|
-
raise ValueError(
|
|
252
|
+
raise ValueError("tag must be a non-empty string")
|
|
252
253
|
|
|
253
254
|
if not isinstance(retries, int) or retries < 1:
|
|
254
|
-
raise ValueError(
|
|
255
|
+
raise ValueError("retries must be a positive int")
|
|
255
256
|
|
|
256
257
|
release = get_release(repo, tag=tag, retries=retries, verbose=verbose)
|
|
257
258
|
return (
|
|
@@ -287,26 +288,24 @@ def list_artifacts(
|
|
|
287
288
|
|
|
288
289
|
Returns
|
|
289
290
|
-------
|
|
290
|
-
A list of dictionaries, each containing information
|
|
291
|
-
by the GitHub API.
|
|
291
|
+
A list of dictionaries, each containing information
|
|
292
|
+
about an artifact as returned by the GitHub API.
|
|
292
293
|
"""
|
|
293
294
|
|
|
294
295
|
if "/" not in repo:
|
|
295
|
-
raise ValueError(
|
|
296
|
+
raise ValueError("repo format must be owner/name")
|
|
296
297
|
|
|
297
298
|
if not isinstance(retries, int) or retries < 1:
|
|
298
|
-
raise ValueError(
|
|
299
|
+
raise ValueError("retries must be a positive int")
|
|
299
300
|
|
|
300
|
-
msg = f"artifact(s) for {repo}" + (
|
|
301
|
-
f" matching name {name}" if name else ""
|
|
302
|
-
)
|
|
301
|
+
msg = f"artifact(s) for {repo}" + (f" matching name {name}" if name else "")
|
|
303
302
|
req_url = f"https://api.github.com/repos/{repo}/actions/artifacts"
|
|
304
303
|
page = 1
|
|
305
304
|
params = {}
|
|
306
305
|
|
|
307
306
|
if name is not None:
|
|
308
307
|
if not isinstance(name, str) or len(name) == 0:
|
|
309
|
-
raise ValueError(
|
|
308
|
+
raise ValueError("name must be a non-empty string")
|
|
310
309
|
params["name"] = name
|
|
311
310
|
|
|
312
311
|
if per_page is not None:
|
|
@@ -336,9 +335,7 @@ def list_artifacts(
|
|
|
336
335
|
# GitHub sometimes returns this error for valid URLs, so retry
|
|
337
336
|
warn(f"URL request try {tries} failed ({err})")
|
|
338
337
|
continue
|
|
339
|
-
raise RuntimeError(
|
|
340
|
-
f"cannot retrieve data from {req_url}"
|
|
341
|
-
) from err
|
|
338
|
+
raise RuntimeError(f"cannot retrieve data from {req_url}") from err
|
|
342
339
|
|
|
343
340
|
artifacts = []
|
|
344
341
|
diff = 1
|
|
@@ -387,10 +384,10 @@ def download_artifact(
|
|
|
387
384
|
"""
|
|
388
385
|
|
|
389
386
|
if "/" not in repo:
|
|
390
|
-
raise ValueError(
|
|
387
|
+
raise ValueError("repo format must be owner/name")
|
|
391
388
|
|
|
392
389
|
if not isinstance(retries, int) or retries < 1:
|
|
393
|
-
raise ValueError(
|
|
390
|
+
raise ValueError("retries must be a positive int")
|
|
394
391
|
|
|
395
392
|
req_url = f"https://api.github.com/repos/{repo}/actions/artifacts/{id}/zip"
|
|
396
393
|
request = urllib.request.Request(req_url)
|
|
@@ -415,9 +412,7 @@ def download_artifact(
|
|
|
415
412
|
warn(f"URL request try {tries} failed ({err})")
|
|
416
413
|
continue
|
|
417
414
|
else:
|
|
418
|
-
raise RuntimeError(
|
|
419
|
-
f"cannot retrieve data from {req_url}"
|
|
420
|
-
) from err
|
|
415
|
+
raise RuntimeError(f"cannot retrieve data from {req_url}") from err
|
|
421
416
|
|
|
422
417
|
if verbose:
|
|
423
418
|
print(f"Uncompressing: {zip_path}")
|
|
@@ -496,14 +491,8 @@ def download_and_unzip(
|
|
|
496
491
|
file_size = int(file_size)
|
|
497
492
|
|
|
498
493
|
bfmt = "{:" + f"{len_file_size}" + ",d}"
|
|
499
|
-
sbfmt = (
|
|
500
|
-
|
|
501
|
-
+ f"{len(bfmt.format(int(file_size)))}"
|
|
502
|
-
+ "s} bytes"
|
|
503
|
-
)
|
|
504
|
-
print(
|
|
505
|
-
f" file size: {sbfmt.format(bfmt.format(int(file_size)))}"
|
|
506
|
-
)
|
|
494
|
+
sbfmt = "{:>" + f"{len(bfmt.format(int(file_size)))}" + "s} bytes"
|
|
495
|
+
print(f" file size: {sbfmt.format(bfmt.format(int(file_size)))}")
|
|
507
496
|
|
|
508
497
|
break
|
|
509
498
|
except urllib.error.HTTPError as err:
|
|
@@ -526,7 +515,7 @@ def download_and_unzip(
|
|
|
526
515
|
|
|
527
516
|
# extract the files
|
|
528
517
|
z.extractall(str(path))
|
|
529
|
-
except:
|
|
518
|
+
except: # noqa: E722
|
|
530
519
|
p = "Could not unzip the file. Stopping."
|
|
531
520
|
raise Exception(p)
|
|
532
521
|
z.close()
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import random
|
|
2
1
|
from collections import OrderedDict
|
|
3
2
|
from itertools import groupby
|
|
4
3
|
from os import PathLike, environ
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
from shutil import copytree, rmtree
|
|
7
|
-
from typing import Dict, List, Optional
|
|
6
|
+
from typing import Dict, Generator, List, Optional
|
|
8
7
|
|
|
9
8
|
from modflow_devtools.imports import import_optional_dependency
|
|
10
9
|
from modflow_devtools.misc import get_namefile_paths, get_packages
|
|
@@ -16,12 +15,8 @@ pytest = import_optional_dependency("pytest")
|
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
@pytest.fixture(scope="function")
|
|
19
|
-
def function_tmpdir(tmpdir_factory, request) -> Path:
|
|
20
|
-
node = (
|
|
21
|
-
request.node.name.replace("/", "_")
|
|
22
|
-
.replace("\\", "_")
|
|
23
|
-
.replace(":", "_")
|
|
24
|
-
)
|
|
18
|
+
def function_tmpdir(tmpdir_factory, request) -> Generator[Path, None, None]:
|
|
19
|
+
node = request.node.name.replace("/", "_").replace("\\", "_").replace(":", "_")
|
|
25
20
|
temp = Path(tmpdir_factory.mktemp(node))
|
|
26
21
|
yield Path(temp)
|
|
27
22
|
|
|
@@ -41,7 +36,7 @@ def function_tmpdir(tmpdir_factory, request) -> Path:
|
|
|
41
36
|
|
|
42
37
|
|
|
43
38
|
@pytest.fixture(scope="class")
|
|
44
|
-
def class_tmpdir(tmpdir_factory, request) -> Path:
|
|
39
|
+
def class_tmpdir(tmpdir_factory, request) -> Generator[Path, None, None]:
|
|
45
40
|
assert (
|
|
46
41
|
request.cls is not None
|
|
47
42
|
), "Class-scoped temp dir fixture must be used on class"
|
|
@@ -57,7 +52,7 @@ def class_tmpdir(tmpdir_factory, request) -> Path:
|
|
|
57
52
|
|
|
58
53
|
|
|
59
54
|
@pytest.fixture(scope="module")
|
|
60
|
-
def module_tmpdir(tmpdir_factory, request) -> Path:
|
|
55
|
+
def module_tmpdir(tmpdir_factory, request) -> Generator[Path, None, None]:
|
|
61
56
|
temp = Path(tmpdir_factory.mktemp(request.module.__name__))
|
|
62
57
|
yield temp
|
|
63
58
|
|
|
@@ -70,7 +65,7 @@ def module_tmpdir(tmpdir_factory, request) -> Path:
|
|
|
70
65
|
|
|
71
66
|
|
|
72
67
|
@pytest.fixture(scope="session")
|
|
73
|
-
def session_tmpdir(tmpdir_factory, request) -> Path:
|
|
68
|
+
def session_tmpdir(tmpdir_factory, request) -> Generator[Path, None, None]:
|
|
74
69
|
temp = Path(tmpdir_factory.mktemp(request.config.rootpath.name))
|
|
75
70
|
yield temp
|
|
76
71
|
|
|
@@ -89,20 +84,7 @@ def repos_path() -> Optional[Path]:
|
|
|
89
84
|
|
|
90
85
|
|
|
91
86
|
@pytest.fixture
|
|
92
|
-
def
|
|
93
|
-
pandas = request.config.option.PANDAS
|
|
94
|
-
if pandas == "yes":
|
|
95
|
-
return True
|
|
96
|
-
elif pandas == "no":
|
|
97
|
-
return False
|
|
98
|
-
elif pandas == "random":
|
|
99
|
-
return random.randint(0, 1) == 0
|
|
100
|
-
else:
|
|
101
|
-
raise ValueError(f"Unsupported value for --pandas: {pandas}")
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@pytest.fixture
|
|
105
|
-
def tabular(request):
|
|
87
|
+
def tabular(request) -> str:
|
|
106
88
|
tab = request.config.option.TABULAR
|
|
107
89
|
if tab not in ["raw", "recarray", "dataframe"]:
|
|
108
90
|
raise ValueError(f"Unsupported value for --tabular: {tab}")
|
|
@@ -119,9 +101,12 @@ def pytest_addoption(parser):
|
|
|
119
101
|
action="store",
|
|
120
102
|
default=None,
|
|
121
103
|
dest="KEEP",
|
|
122
|
-
help="Move the contents of temporary test directories to
|
|
123
|
-
"
|
|
124
|
-
"
|
|
104
|
+
help="Move the contents of temporary test directories to "
|
|
105
|
+
"correspondingly named subdirectories at the given "
|
|
106
|
+
"location after tests complete. This option can be used "
|
|
107
|
+
"to exclude test results from automatic cleanup, "
|
|
108
|
+
"e.g. for manual inspection. The provided path is "
|
|
109
|
+
"created if it does not already exist. An error is "
|
|
125
110
|
"thrown if any matching files already exist.",
|
|
126
111
|
)
|
|
127
112
|
|
|
@@ -129,9 +114,12 @@ def pytest_addoption(parser):
|
|
|
129
114
|
"--keep-failed",
|
|
130
115
|
action="store",
|
|
131
116
|
default=None,
|
|
132
|
-
help="Move the contents of temporary test directories to
|
|
133
|
-
"
|
|
134
|
-
"
|
|
117
|
+
help="Move the contents of temporary test directories to "
|
|
118
|
+
"correspondingly named subdirectories at the given "
|
|
119
|
+
"location if the test case fails. This option saves "
|
|
120
|
+
"the outputs of failed tests in the "
|
|
121
|
+
"given location. The path is created if it doesn't "
|
|
122
|
+
"already exist. An error is thrown if files with the "
|
|
135
123
|
"same names already exist in the given location.",
|
|
136
124
|
)
|
|
137
125
|
|
|
@@ -148,7 +136,8 @@ def pytest_addoption(parser):
|
|
|
148
136
|
"--meta",
|
|
149
137
|
action="store",
|
|
150
138
|
metavar="NAME",
|
|
151
|
-
help="Indicates a test should only be run by other tests (e.g.,
|
|
139
|
+
help="Indicates a test should only be run by other tests (e.g., "
|
|
140
|
+
"to test framework or fixtures).",
|
|
152
141
|
)
|
|
153
142
|
|
|
154
143
|
parser.addoption(
|
|
@@ -165,22 +154,14 @@ def pytest_addoption(parser):
|
|
|
165
154
|
help="Select a subset of packages to run.",
|
|
166
155
|
)
|
|
167
156
|
|
|
168
|
-
parser.addoption(
|
|
169
|
-
"-P",
|
|
170
|
-
"--pandas",
|
|
171
|
-
action="store",
|
|
172
|
-
default="yes",
|
|
173
|
-
dest="PANDAS",
|
|
174
|
-
help="Indicates whether to use pandas, where multiple approaches are available. Select 'yes', 'no', or 'random'.",
|
|
175
|
-
)
|
|
176
|
-
|
|
177
157
|
parser.addoption(
|
|
178
158
|
"-T",
|
|
179
159
|
"--tabular",
|
|
180
160
|
action="store",
|
|
181
161
|
default="raw",
|
|
182
162
|
dest="TABULAR",
|
|
183
|
-
help="Configure tabular data representation for model input.
|
|
163
|
+
help="Configure tabular data representation for model input. "
|
|
164
|
+
"Select 'raw', 'recarray', or 'dataframe'.",
|
|
184
165
|
)
|
|
185
166
|
|
|
186
167
|
|
|
@@ -269,9 +250,7 @@ def pytest_generate_tests(metafunc):
|
|
|
269
250
|
if repo_path
|
|
270
251
|
else []
|
|
271
252
|
)
|
|
272
|
-
metafunc.parametrize(
|
|
273
|
-
key, namefile_paths, ids=[str(m) for m in namefile_paths]
|
|
274
|
-
)
|
|
253
|
+
metafunc.parametrize(key, namefile_paths, ids=[str(m) for m in namefile_paths])
|
|
275
254
|
|
|
276
255
|
key = "test_model_mf5to6"
|
|
277
256
|
if key in metafunc.fixturenames:
|
|
@@ -288,9 +267,7 @@ def pytest_generate_tests(metafunc):
|
|
|
288
267
|
if repo_path
|
|
289
268
|
else []
|
|
290
269
|
)
|
|
291
|
-
metafunc.parametrize(
|
|
292
|
-
key, namefile_paths, ids=[str(m) for m in namefile_paths]
|
|
293
|
-
)
|
|
270
|
+
metafunc.parametrize(key, namefile_paths, ids=[str(m) for m in namefile_paths])
|
|
294
271
|
|
|
295
272
|
key = "large_test_model"
|
|
296
273
|
if key in metafunc.fixturenames:
|
|
@@ -307,9 +284,7 @@ def pytest_generate_tests(metafunc):
|
|
|
307
284
|
if repo_path
|
|
308
285
|
else []
|
|
309
286
|
)
|
|
310
|
-
metafunc.parametrize(
|
|
311
|
-
key, namefile_paths, ids=[str(m) for m in namefile_paths]
|
|
312
|
-
)
|
|
287
|
+
metafunc.parametrize(key, namefile_paths, ids=[str(m) for m in namefile_paths])
|
|
313
288
|
|
|
314
289
|
key = "example_scenario"
|
|
315
290
|
if key in metafunc.fixturenames:
|
|
@@ -384,9 +359,7 @@ def pytest_generate_tests(metafunc):
|
|
|
384
359
|
filtered.append(name)
|
|
385
360
|
break
|
|
386
361
|
examples = {
|
|
387
|
-
name: nfps
|
|
388
|
-
for name, nfps in examples.items()
|
|
389
|
-
if name in filtered
|
|
362
|
+
name: nfps for name, nfps in examples.items() if name in filtered
|
|
390
363
|
}
|
|
391
364
|
|
|
392
365
|
# exclude mf6gwf and mf6gwt subdirs
|
|
@@ -402,5 +375,5 @@ def pytest_generate_tests(metafunc):
|
|
|
402
375
|
metafunc.parametrize(
|
|
403
376
|
key,
|
|
404
377
|
[(name, nfps) for name, nfps in example_scenarios.items()],
|
|
405
|
-
ids=
|
|
378
|
+
ids=list(example_scenarios.keys()),
|
|
406
379
|
)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# This file is dual licensed under the terms of the BSD 3-Clause License.
|
|
4
4
|
# BSD 3-Clause License
|
|
5
5
|
#
|
|
6
|
-
# Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
|
|
6
|
+
# Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team # noqa: E501
|
|
7
7
|
# All rights reserved.
|
|
8
8
|
#
|
|
9
9
|
# Copyright (c) 2011-2021, Open source contributors.
|
|
@@ -130,9 +130,7 @@ def import_optional_dependency(
|
|
|
130
130
|
module_to_get = sys.modules[install_name]
|
|
131
131
|
else:
|
|
132
132
|
module_to_get = module
|
|
133
|
-
minimum_version = (
|
|
134
|
-
min_version if min_version is not None else VERSIONS.get(parent)
|
|
135
|
-
)
|
|
133
|
+
minimum_version = min_version if min_version is not None else VERSIONS.get(parent)
|
|
136
134
|
if minimum_version:
|
|
137
135
|
version = get_version(module_to_get)
|
|
138
136
|
if Version(version) < Version(minimum_version):
|
|
@@ -1,13 +1,38 @@
|
|
|
1
|
-
import
|
|
1
|
+
from os import PathLike
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Iterable, Union
|
|
2
4
|
|
|
3
5
|
|
|
4
|
-
def build_table(
|
|
6
|
+
def build_table(
|
|
7
|
+
caption: str,
|
|
8
|
+
fpth: Union[str, PathLike],
|
|
9
|
+
arr,
|
|
10
|
+
headings: Iterable[str] = None,
|
|
11
|
+
col_widths: Iterable[float] = None,
|
|
12
|
+
):
|
|
13
|
+
"""
|
|
14
|
+
Build a LaTeX table from the given NumPy array.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
caption : str
|
|
19
|
+
The table's caption
|
|
20
|
+
fpth : str or path-like
|
|
21
|
+
The LaTeX file to create
|
|
22
|
+
arr : numpy recarray
|
|
23
|
+
The array
|
|
24
|
+
headings : iterable of str
|
|
25
|
+
The table headings
|
|
26
|
+
col_widths : iterable of float
|
|
27
|
+
The table's column widths
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
fpth = Path(fpth).expanduser().absolute().with_suffix(".tex")
|
|
31
|
+
|
|
5
32
|
if headings is None:
|
|
6
33
|
headings = arr.dtype.names
|
|
7
34
|
ncols = len(arr.dtype.names)
|
|
8
|
-
|
|
9
|
-
fpth += ".tex"
|
|
10
|
-
label = "tab:{}".format(os.path.basename(fpth).replace(".tex", ""))
|
|
35
|
+
label = "tab:{}".format(fpth.stem)
|
|
11
36
|
|
|
12
37
|
line = get_header(caption, label, headings, col_widths=col_widths)
|
|
13
38
|
|
|
@@ -29,8 +54,32 @@ def build_table(caption, fpth, arr, headings=None, col_widths=None):
|
|
|
29
54
|
|
|
30
55
|
|
|
31
56
|
def get_header(
|
|
32
|
-
caption
|
|
57
|
+
caption: str,
|
|
58
|
+
label: str,
|
|
59
|
+
headings: Iterable[str],
|
|
60
|
+
col_widths: Iterable[float] = None,
|
|
61
|
+
center: bool = True,
|
|
62
|
+
firsthead: bool = False,
|
|
33
63
|
):
|
|
64
|
+
"""
|
|
65
|
+
Build a LaTeX table header.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
caption : str
|
|
70
|
+
The table's caption
|
|
71
|
+
label : str
|
|
72
|
+
The table's label
|
|
73
|
+
headings : iterable of str
|
|
74
|
+
The table's heading
|
|
75
|
+
col_widths : iterable of float
|
|
76
|
+
The table's column widths
|
|
77
|
+
center : bool
|
|
78
|
+
Whether to center-align the table text
|
|
79
|
+
firsthead : bool
|
|
80
|
+
Whether to add first header
|
|
81
|
+
"""
|
|
82
|
+
|
|
34
83
|
ncol = len(headings)
|
|
35
84
|
if col_widths is None:
|
|
36
85
|
dx = 0.8 / float(ncol)
|
|
@@ -43,11 +92,7 @@ def get_header(
|
|
|
43
92
|
header = "\\small\n"
|
|
44
93
|
header += "\\begin{longtable}[!htbp]{\n"
|
|
45
94
|
for col_width in col_widths:
|
|
46
|
-
header +=
|
|
47
|
-
38 * " "
|
|
48
|
-
+ f"{align}"
|
|
49
|
-
+ f"{{{col_width}\\linewidth-2\\arraycolsep}}\n"
|
|
50
|
-
)
|
|
95
|
+
header += 38 * " " + f"{align}" + f"{{{col_width}\\linewidth-2\\arraycolsep}}\n"
|
|
51
96
|
header += 38 * " " + "}\n"
|
|
52
97
|
header += f"\t\\caption{{{caption}}} \\label{{{label}}} \\\\\n\n"
|
|
53
98
|
|
|
@@ -85,7 +130,6 @@ def exp_format(v):
|
|
|
85
130
|
s = f"{v:.2e}"
|
|
86
131
|
s = s.replace("e-0", "e-")
|
|
87
132
|
s = s.replace("e+0", "e+")
|
|
88
|
-
# s = s.replace("e", " \\times 10^{") + "}$"
|
|
89
133
|
return s
|
|
90
134
|
|
|
91
135
|
|
|
@@ -3,7 +3,9 @@ Pytest markers to toggle tests based on environment conditions.
|
|
|
3
3
|
Occasionally useful to directly assert environment expectations.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from os import environ
|
|
6
7
|
from platform import python_version, system
|
|
8
|
+
from typing import Dict, Optional
|
|
7
9
|
|
|
8
10
|
from packaging.version import Version
|
|
9
11
|
|
|
@@ -31,7 +33,7 @@ def requires_exe(*exes):
|
|
|
31
33
|
|
|
32
34
|
def requires_python(version, bound="lower"):
|
|
33
35
|
if not isinstance(version, str):
|
|
34
|
-
raise ValueError(
|
|
36
|
+
raise ValueError("Version must a string")
|
|
35
37
|
|
|
36
38
|
py_tgt = Version(version)
|
|
37
39
|
if bound == "lower":
|
|
@@ -46,8 +48,8 @@ def requires_python(version, bound="lower"):
|
|
|
46
48
|
)
|
|
47
49
|
|
|
48
50
|
|
|
49
|
-
def requires_pkg(*pkgs):
|
|
50
|
-
missing = {pkg for pkg in pkgs if not has_pkg(pkg, strict=True)}
|
|
51
|
+
def requires_pkg(*pkgs, name_map: Optional[Dict[str, str]] = None):
|
|
52
|
+
missing = {pkg for pkg in pkgs if not has_pkg(pkg, strict=True, name_map=name_map)}
|
|
51
53
|
return pytest.mark.skipif(
|
|
52
54
|
missing,
|
|
53
55
|
reason=f"missing package{'s' if len(missing) != 1 else ''}: "
|
|
@@ -57,25 +59,21 @@ def requires_pkg(*pkgs):
|
|
|
57
59
|
|
|
58
60
|
def requires_platform(platform, ci_only=False):
|
|
59
61
|
return pytest.mark.skipif(
|
|
60
|
-
system().lower() != platform.lower()
|
|
61
|
-
and (is_in_ci() if ci_only else True),
|
|
62
|
+
system().lower() != platform.lower() and (is_in_ci() if ci_only else True),
|
|
62
63
|
reason=f"only compatible with platform: {platform.lower()}",
|
|
63
64
|
)
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
def excludes_platform(platform, ci_only=False):
|
|
67
68
|
return pytest.mark.skipif(
|
|
68
|
-
system().lower() == platform.lower()
|
|
69
|
-
and (is_in_ci() if ci_only else True),
|
|
69
|
+
system().lower() == platform.lower() and (is_in_ci() if ci_only else True),
|
|
70
70
|
reason=f"not compatible with platform: {platform.lower()}",
|
|
71
71
|
)
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
def requires_branch(branch):
|
|
75
75
|
current = get_current_branch()
|
|
76
|
-
return pytest.mark.skipif(
|
|
77
|
-
current != branch, reason=f"must run on branch: {branch}"
|
|
78
|
-
)
|
|
76
|
+
return pytest.mark.skipif(current != branch, reason=f"must run on branch: {branch}")
|
|
79
77
|
|
|
80
78
|
|
|
81
79
|
def excludes_branch(branch):
|
|
@@ -85,6 +83,11 @@ def excludes_branch(branch):
|
|
|
85
83
|
)
|
|
86
84
|
|
|
87
85
|
|
|
86
|
+
no_parallel = pytest.mark.skipif(
|
|
87
|
+
environ.get("PYTEST_XDIST_WORKER_COUNT"), reason="can't run in parallel"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
88
91
|
requires_github = pytest.mark.skipif(
|
|
89
92
|
not is_connected("github.com"), reason="github.com is required."
|
|
90
93
|
)
|
|
@@ -12,8 +12,9 @@ from pathlib import Path
|
|
|
12
12
|
from shutil import which
|
|
13
13
|
from subprocess import PIPE, Popen
|
|
14
14
|
from timeit import timeit
|
|
15
|
-
from typing import List, Optional, Tuple
|
|
15
|
+
from typing import Dict, List, Optional, Tuple
|
|
16
16
|
from urllib import request
|
|
17
|
+
from urllib.error import URLError
|
|
17
18
|
|
|
18
19
|
from _warnings import warn
|
|
19
20
|
|
|
@@ -70,7 +71,8 @@ def get_ostag() -> str:
|
|
|
70
71
|
|
|
71
72
|
def get_suffixes(ostag) -> Tuple[str, str]:
|
|
72
73
|
"""
|
|
73
|
-
Returns executable and library suffixes for the
|
|
74
|
+
Returns executable and library suffixes for the
|
|
75
|
+
given OS (as returned by sys.platform)
|
|
74
76
|
|
|
75
77
|
.. deprecated:: 1.1.0
|
|
76
78
|
Use ``ostags.get_binary_suffixes()`` instead.
|
|
@@ -150,9 +152,11 @@ def get_current_branch() -> str:
|
|
|
150
152
|
|
|
151
153
|
def get_packages(namefile_path: PathLike) -> List[str]:
|
|
152
154
|
"""
|
|
153
|
-
Return a list of packages used by the simulation
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
Return a list of packages used by the simulation
|
|
156
|
+
or model defined in the given namefile. The namefile
|
|
157
|
+
may be for an entire simulation or for a GWF or GWT
|
|
158
|
+
model. If a simulation namefile is given, packages
|
|
159
|
+
used in its component model namefiles will be included.
|
|
156
160
|
|
|
157
161
|
Parameters
|
|
158
162
|
----------
|
|
@@ -167,38 +171,35 @@ def get_packages(namefile_path: PathLike) -> List[str]:
|
|
|
167
171
|
packages = []
|
|
168
172
|
path = Path(namefile_path).expanduser().absolute()
|
|
169
173
|
lines = open(path, "r").readlines()
|
|
170
|
-
gwf_lines = [
|
|
171
|
-
gwt_lines = [
|
|
174
|
+
gwf_lines = [ln for ln in lines if ln.strip().lower().startswith("gwf6 ")]
|
|
175
|
+
gwt_lines = [ln for ln in lines if ln.strip().lower().startswith("gwt6 ")]
|
|
172
176
|
|
|
173
177
|
def parse_model_namefile(line):
|
|
174
178
|
nf_path = [path.parent / s for s in line.split(" ") if s != ""][1]
|
|
175
179
|
if nf_path.suffix != ".nam":
|
|
176
180
|
raise ValueError(
|
|
177
|
-
|
|
181
|
+
"Failed to parse GWF or GWT model namefile "
|
|
182
|
+
f"from simulation namefile line: {line}"
|
|
178
183
|
)
|
|
179
184
|
return nf_path
|
|
180
185
|
|
|
181
186
|
# load model namefiles
|
|
182
187
|
try:
|
|
183
188
|
for line in gwf_lines:
|
|
184
|
-
packages = (
|
|
185
|
-
packages + get_packages(parse_model_namefile(line)) + ["gwf"]
|
|
186
|
-
)
|
|
189
|
+
packages = packages + get_packages(parse_model_namefile(line)) + ["gwf"]
|
|
187
190
|
for line in gwt_lines:
|
|
188
|
-
packages = (
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
except:
|
|
191
|
+
packages = packages + get_packages(parse_model_namefile(line)) + ["gwt"]
|
|
192
|
+
except: # noqa: E722
|
|
192
193
|
warn(f"Invalid namefile format: {traceback.format_exc()}")
|
|
193
194
|
|
|
194
195
|
for line in lines:
|
|
195
196
|
# Skip over blank and commented lines
|
|
196
|
-
|
|
197
|
-
if len(
|
|
197
|
+
line = line.strip().split()
|
|
198
|
+
if len(line) < 2:
|
|
198
199
|
continue
|
|
199
200
|
|
|
200
|
-
|
|
201
|
-
if any(
|
|
201
|
+
line = line[0].lower()
|
|
202
|
+
if any(line.startswith(c) for c in ["#", "!", "data", "list"]) or line in [
|
|
202
203
|
"begin",
|
|
203
204
|
"end",
|
|
204
205
|
"memory_print_option",
|
|
@@ -206,9 +207,9 @@ def get_packages(namefile_path: PathLike) -> List[str]:
|
|
|
206
207
|
continue
|
|
207
208
|
|
|
208
209
|
# strip "6" from package name
|
|
209
|
-
|
|
210
|
+
line = line.replace("6", "")
|
|
210
211
|
|
|
211
|
-
packages.append(
|
|
212
|
+
packages.append(line.lower())
|
|
212
213
|
|
|
213
214
|
return list(set(packages))
|
|
214
215
|
|
|
@@ -242,17 +243,12 @@ def get_namefile_paths(
|
|
|
242
243
|
|
|
243
244
|
# find simulation namefiles
|
|
244
245
|
paths = [
|
|
245
|
-
p
|
|
246
|
-
for p in Path(path).rglob(
|
|
247
|
-
f"{prefix}*/**/{namefile}" if prefix else namefile
|
|
248
|
-
)
|
|
246
|
+
p for p in Path(path).rglob(f"{prefix}*/**/{namefile}" if prefix else namefile)
|
|
249
247
|
]
|
|
250
248
|
|
|
251
249
|
# remove excluded
|
|
252
250
|
paths = [
|
|
253
|
-
p
|
|
254
|
-
for p in paths
|
|
255
|
-
if (not excluded or not any(e in str(p) for e in excluded))
|
|
251
|
+
p for p in paths if (not excluded or not any(e in str(p) for e in excluded))
|
|
256
252
|
]
|
|
257
253
|
|
|
258
254
|
# filter by package
|
|
@@ -260,9 +256,7 @@ def get_namefile_paths(
|
|
|
260
256
|
filtered = []
|
|
261
257
|
for nfp in paths:
|
|
262
258
|
nf_pkgs = get_packages(nfp)
|
|
263
|
-
shared = set(nf_pkgs).intersection(
|
|
264
|
-
set([p.lower() for p in packages])
|
|
265
|
-
)
|
|
259
|
+
shared = set(nf_pkgs).intersection(set([p.lower() for p in packages]))
|
|
266
260
|
if any(shared):
|
|
267
261
|
filtered.append(nfp)
|
|
268
262
|
paths = filtered
|
|
@@ -271,9 +265,7 @@ def get_namefile_paths(
|
|
|
271
265
|
if selected:
|
|
272
266
|
paths = [
|
|
273
267
|
namfile_path
|
|
274
|
-
for (namfile_path, model_path) in zip(
|
|
275
|
-
paths, [p.parent for p in paths]
|
|
276
|
-
)
|
|
268
|
+
for (namfile_path, model_path) in zip(paths, [p.parent for p in paths])
|
|
277
269
|
if any(s in model_path.name for s in selected)
|
|
278
270
|
]
|
|
279
271
|
|
|
@@ -292,15 +284,41 @@ def get_model_paths(
|
|
|
292
284
|
Find model directories recursively in the given location.
|
|
293
285
|
A model directory is any directory containing one or more
|
|
294
286
|
namefiles. Model directories can be filtered or excluded,
|
|
295
|
-
by prefix, pattern, namefile name, or packages used.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
287
|
+
by prefix, pattern, namefile name, or packages used. The
|
|
288
|
+
directories are returned in order within scenario folders
|
|
289
|
+
such that groundwater flow model workspaces precede other
|
|
290
|
+
model types. This allows models which depend on the flow
|
|
291
|
+
model's outputs to consume its head or budget, and models
|
|
292
|
+
should successfully run in the sequence returned provided
|
|
293
|
+
input files (e.g. FMI) refer to output via relative paths.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
def keyfunc(v):
|
|
297
|
+
v = str(v)
|
|
298
|
+
if "gwf" in v:
|
|
299
|
+
return 0
|
|
300
|
+
else:
|
|
301
|
+
return 1
|
|
302
|
+
|
|
303
|
+
model_paths = []
|
|
304
|
+
globbed = path.rglob(f"{prefix if prefix else ''}*")
|
|
305
|
+
example_paths = [p for p in globbed if p.is_dir()]
|
|
306
|
+
for p in example_paths:
|
|
307
|
+
for mp in sorted(
|
|
308
|
+
list(
|
|
309
|
+
set(
|
|
310
|
+
[
|
|
311
|
+
p.parent
|
|
312
|
+
for p in get_namefile_paths(
|
|
313
|
+
p, prefix, namefile, excluded, selected, packages
|
|
314
|
+
)
|
|
315
|
+
]
|
|
316
|
+
)
|
|
317
|
+
),
|
|
318
|
+
key=keyfunc,
|
|
319
|
+
):
|
|
320
|
+
if mp not in model_paths:
|
|
321
|
+
model_paths.append(mp)
|
|
304
322
|
return model_paths
|
|
305
323
|
|
|
306
324
|
|
|
@@ -338,19 +356,19 @@ def is_github_rate_limited() -> Optional[bool]:
|
|
|
338
356
|
|
|
339
357
|
Returns
|
|
340
358
|
-------
|
|
341
|
-
True if rate-limiting is applied, otherwise False
|
|
359
|
+
True if rate-limiting is applied, otherwise False
|
|
360
|
+
(or None if the connection fails).
|
|
342
361
|
"""
|
|
343
362
|
try:
|
|
344
|
-
with request.urlopen(
|
|
345
|
-
"https://api.github.com/users/octocat"
|
|
346
|
-
) as response:
|
|
363
|
+
with request.urlopen("https://api.github.com/users/octocat") as response:
|
|
347
364
|
remaining = int(response.headers["x-ratelimit-remaining"])
|
|
348
365
|
if remaining < 10:
|
|
349
366
|
warn(
|
|
350
|
-
f"Only {remaining} GitHub API requests
|
|
367
|
+
f"Only {remaining} GitHub API requests "
|
|
368
|
+
"remaining before rate-limiting"
|
|
351
369
|
)
|
|
352
370
|
return remaining > 0
|
|
353
|
-
except:
|
|
371
|
+
except (ValueError, URLError):
|
|
354
372
|
return None
|
|
355
373
|
|
|
356
374
|
|
|
@@ -369,7 +387,9 @@ def has_exe(exe):
|
|
|
369
387
|
return _has_exe_cache[exe]
|
|
370
388
|
|
|
371
389
|
|
|
372
|
-
def has_pkg(
|
|
390
|
+
def has_pkg(
|
|
391
|
+
pkg: str, strict: bool = False, name_map: Optional[Dict[str, str]] = None
|
|
392
|
+
) -> bool:
|
|
373
393
|
"""
|
|
374
394
|
Determines if the given Python package is installed.
|
|
375
395
|
|
|
@@ -378,8 +398,13 @@ def has_pkg(pkg: str, strict: bool = False) -> bool:
|
|
|
378
398
|
pkg : str
|
|
379
399
|
Name of the package to check.
|
|
380
400
|
strict : bool
|
|
381
|
-
If False, only check if package metadata is available.
|
|
401
|
+
If False, only check if the package is cached or metadata is available.
|
|
382
402
|
If True, try to import the package (all dependencies must be present).
|
|
403
|
+
name_map : dict, optional
|
|
404
|
+
Custom mapping between package names (as provided to `metadata.distribution`)
|
|
405
|
+
and module names (as used in import statements or `importlib.import_module`).
|
|
406
|
+
Useful for packages whose package names do not match the module name, e.g.
|
|
407
|
+
`pytest-xdist` and `xdist`, respectively, or `mfpymake` and `pymake`.
|
|
383
408
|
|
|
384
409
|
Returns
|
|
385
410
|
-------
|
|
@@ -388,12 +413,19 @@ def has_pkg(pkg: str, strict: bool = False) -> bool:
|
|
|
388
413
|
|
|
389
414
|
Notes
|
|
390
415
|
-----
|
|
416
|
+
If `strict=True` and a package name differs from its top-level module name, a
|
|
417
|
+
`name_map` must be provided, otherwise this function will return False even if
|
|
418
|
+
the package is installed.
|
|
419
|
+
|
|
391
420
|
Originally written by Mike Toews (mwtoews@gmail.com) for FloPy.
|
|
392
421
|
"""
|
|
393
422
|
|
|
394
|
-
def
|
|
423
|
+
def get_module_name() -> str:
|
|
424
|
+
return pkg if name_map is None else name_map.get(pkg, pkg)
|
|
425
|
+
|
|
426
|
+
def try_import() -> bool:
|
|
395
427
|
try: # import name, e.g. "import shapefile"
|
|
396
|
-
importlib.import_module(
|
|
428
|
+
importlib.import_module(get_module_name())
|
|
397
429
|
return True
|
|
398
430
|
except ModuleNotFoundError:
|
|
399
431
|
return False
|
|
@@ -405,14 +437,15 @@ def has_pkg(pkg: str, strict: bool = False) -> bool:
|
|
|
405
437
|
except metadata.PackageNotFoundError:
|
|
406
438
|
return False
|
|
407
439
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if
|
|
412
|
-
found =
|
|
440
|
+
is_cached = pkg in _has_pkg_cache
|
|
441
|
+
has_metadata = try_metadata()
|
|
442
|
+
can_import = try_import()
|
|
443
|
+
if strict:
|
|
444
|
+
found = has_metadata and can_import
|
|
445
|
+
else:
|
|
446
|
+
found = has_metadata or is_cached
|
|
413
447
|
_has_pkg_cache[pkg] = found
|
|
414
|
-
|
|
415
|
-
return _has_pkg_cache[pkg]
|
|
448
|
+
return found
|
|
416
449
|
|
|
417
450
|
|
|
418
451
|
def timed(f):
|
|
@@ -481,7 +514,14 @@ def get_env(name: str, default: object = None) -> Optional[object]:
|
|
|
481
514
|
if isinstance(default, bool):
|
|
482
515
|
v = v.lower().title()
|
|
483
516
|
v = literal_eval(v)
|
|
484
|
-
except
|
|
517
|
+
except (
|
|
518
|
+
AttributeError,
|
|
519
|
+
ValueError,
|
|
520
|
+
TypeError,
|
|
521
|
+
SyntaxError,
|
|
522
|
+
MemoryError,
|
|
523
|
+
RecursionError,
|
|
524
|
+
):
|
|
485
525
|
return default
|
|
486
526
|
if default is None:
|
|
487
527
|
return v
|
|
@@ -73,10 +73,10 @@ def get_binary_suffixes(ostag: str = None) -> Tuple[str, str]:
|
|
|
73
73
|
|
|
74
74
|
try:
|
|
75
75
|
return _suffixes(ostag.lower())
|
|
76
|
-
except:
|
|
76
|
+
except KeyError:
|
|
77
77
|
try:
|
|
78
78
|
return _suffixes(python_to_modflow_ostag(ostag))
|
|
79
|
-
except:
|
|
79
|
+
except KeyError:
|
|
80
80
|
return _suffixes(github_to_modflow_ostag(ostag))
|
|
81
81
|
|
|
82
82
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from io import BytesIO, StringIO
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from modflow_devtools.imports import import_optional_dependency
|
|
5
|
+
|
|
6
|
+
np = import_optional_dependency("numpy")
|
|
7
|
+
pytest = import_optional_dependency("pytest")
|
|
8
|
+
syrupy = import_optional_dependency("syrupy")
|
|
9
|
+
|
|
10
|
+
# ruff: noqa: E402
|
|
11
|
+
from syrupy import __import_extension
|
|
12
|
+
from syrupy.assertion import SnapshotAssertion
|
|
13
|
+
from syrupy.extensions.single_file import (
|
|
14
|
+
SingleFileSnapshotExtension,
|
|
15
|
+
WriteMode,
|
|
16
|
+
)
|
|
17
|
+
from syrupy.location import PyTestLocation
|
|
18
|
+
from syrupy.types import (
|
|
19
|
+
PropertyFilter,
|
|
20
|
+
PropertyMatcher,
|
|
21
|
+
SerializableData,
|
|
22
|
+
SerializedData,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# extension classes
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BinaryArrayExtension(SingleFileSnapshotExtension):
|
|
29
|
+
"""
|
|
30
|
+
Binary snapshot of a NumPy array. Can be read back into NumPy with
|
|
31
|
+
.load(), preserving dtype and shape. This is the recommended array
|
|
32
|
+
snapshot approach if human-readability is not a necessity, as disk
|
|
33
|
+
space is minimized.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
_write_mode = WriteMode.BINARY
|
|
37
|
+
_file_extension = "npy"
|
|
38
|
+
|
|
39
|
+
def serialize(
|
|
40
|
+
self,
|
|
41
|
+
data,
|
|
42
|
+
*,
|
|
43
|
+
exclude=None,
|
|
44
|
+
include=None,
|
|
45
|
+
matcher=None,
|
|
46
|
+
):
|
|
47
|
+
buffer = BytesIO()
|
|
48
|
+
np.save(buffer, data)
|
|
49
|
+
return buffer.getvalue()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TextArrayExtension(SingleFileSnapshotExtension):
|
|
53
|
+
"""
|
|
54
|
+
Text snapshot of a NumPy array. Flattens the array before writing.
|
|
55
|
+
Can be read back into NumPy with .loadtxt() assuming you know the
|
|
56
|
+
shape of the expected data and subsequently reshape it if needed.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
_write_mode = WriteMode.TEXT
|
|
60
|
+
_file_extension = "txt"
|
|
61
|
+
|
|
62
|
+
def serialize(
|
|
63
|
+
self,
|
|
64
|
+
data: "SerializableData",
|
|
65
|
+
*,
|
|
66
|
+
exclude: Optional["PropertyFilter"] = None,
|
|
67
|
+
include: Optional["PropertyFilter"] = None,
|
|
68
|
+
matcher: Optional["PropertyMatcher"] = None,
|
|
69
|
+
) -> "SerializedData":
|
|
70
|
+
buffer = StringIO()
|
|
71
|
+
np.savetxt(buffer, data.ravel())
|
|
72
|
+
return buffer.getvalue()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ReadableArrayExtension(SingleFileSnapshotExtension):
|
|
76
|
+
"""
|
|
77
|
+
Human-readable snapshot of a NumPy array. Preserves array shape
|
|
78
|
+
at the expense of possible loss of precision (default 8 places)
|
|
79
|
+
and more difficulty loading into NumPy than TextArrayExtension.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
_write_mode = WriteMode.TEXT
|
|
83
|
+
_file_extension = "txt"
|
|
84
|
+
|
|
85
|
+
def serialize(
|
|
86
|
+
self,
|
|
87
|
+
data: "SerializableData",
|
|
88
|
+
*,
|
|
89
|
+
exclude: Optional["PropertyFilter"] = None,
|
|
90
|
+
include: Optional["PropertyFilter"] = None,
|
|
91
|
+
matcher: Optional["PropertyMatcher"] = None,
|
|
92
|
+
) -> "SerializedData":
|
|
93
|
+
return np.array2string(data, threshold=np.inf)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class MatchAnything:
|
|
97
|
+
def __eq__(self, _):
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# fixtures
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.fixture(scope="session")
|
|
105
|
+
def snapshot_disable(pytestconfig) -> bool:
|
|
106
|
+
return pytestconfig.getoption("--snapshot-disable")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.fixture
|
|
110
|
+
def snapshot(request, snapshot_disable) -> "SnapshotAssertion":
|
|
111
|
+
return (
|
|
112
|
+
MatchAnything()
|
|
113
|
+
if snapshot_disable
|
|
114
|
+
else SnapshotAssertion(
|
|
115
|
+
update_snapshots=request.config.option.update_snapshots,
|
|
116
|
+
extension_class=__import_extension(request.config.option.default_extension),
|
|
117
|
+
test_location=PyTestLocation(request.node),
|
|
118
|
+
session=request.session.config._syrupy,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.fixture
|
|
124
|
+
def array_snapshot(snapshot, snapshot_disable):
|
|
125
|
+
return (
|
|
126
|
+
MatchAnything()
|
|
127
|
+
if snapshot_disable
|
|
128
|
+
else snapshot.use_extension(BinaryArrayExtension)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.fixture
|
|
133
|
+
def text_array_snapshot(snapshot, snapshot_disable):
|
|
134
|
+
return (
|
|
135
|
+
MatchAnything()
|
|
136
|
+
if snapshot_disable
|
|
137
|
+
else snapshot.use_extension(TextArrayExtension)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.fixture
|
|
142
|
+
def readable_array_snapshot(snapshot, snapshot_disable):
|
|
143
|
+
return (
|
|
144
|
+
MatchAnything()
|
|
145
|
+
if snapshot_disable
|
|
146
|
+
else snapshot.use_extension(ReadableArrayExtension)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# pytest config hooks
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def pytest_addoption(parser):
|
|
154
|
+
parser.addoption(
|
|
155
|
+
"--snapshot-disable",
|
|
156
|
+
action="store_true",
|
|
157
|
+
default=False,
|
|
158
|
+
help="Disable snapshot comparisons.",
|
|
159
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: modflow-devtools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Python tools for MODFLOW development
|
|
5
5
|
Author-email: "Joseph D. Hughes" <modflow@usgs.gov>, Michael Reno <mreno@ucar.edu>, Mike Taves <mwtoews@gmail.com>, Wes Bonelli <wbonelli@ucar.edu>
|
|
6
6
|
Maintainer-email: "Joseph D. Hughes" <modflow@usgs.gov>
|
|
@@ -24,11 +24,7 @@ Requires-Python: >=3.8
|
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE.md
|
|
26
26
|
Provides-Extra: lint
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist: cffconvert; extra == "lint"
|
|
29
|
-
Requires-Dist: flake8; extra == "lint"
|
|
30
|
-
Requires-Dist: isort; extra == "lint"
|
|
31
|
-
Requires-Dist: pylint; extra == "lint"
|
|
27
|
+
Requires-Dist: ruff; extra == "lint"
|
|
32
28
|
Provides-Extra: test
|
|
33
29
|
Requires-Dist: modflow-devtools[lint]; extra == "test"
|
|
34
30
|
Requires-Dist: coverage; extra == "test"
|
|
@@ -37,11 +33,13 @@ Requires-Dist: filelock; extra == "test"
|
|
|
37
33
|
Requires-Dist: meson!=0.63.0; extra == "test"
|
|
38
34
|
Requires-Dist: ninja; extra == "test"
|
|
39
35
|
Requires-Dist: numpy; extra == "test"
|
|
40
|
-
Requires-Dist:
|
|
36
|
+
Requires-Dist: pandas; extra == "test"
|
|
37
|
+
Requires-Dist: pytest!=8.1.0; extra == "test"
|
|
41
38
|
Requires-Dist: pytest-cov; extra == "test"
|
|
42
39
|
Requires-Dist: pytest-dotenv; extra == "test"
|
|
43
40
|
Requires-Dist: pytest-xdist; extra == "test"
|
|
44
41
|
Requires-Dist: PyYaml; extra == "test"
|
|
42
|
+
Requires-Dist: syrupy; extra == "test"
|
|
45
43
|
Provides-Extra: docs
|
|
46
44
|
Requires-Dist: sphinx; extra == "docs"
|
|
47
45
|
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
@@ -105,6 +103,7 @@ Python3.8+, dependency-free, but pairs well with `pytest` and select plugins, e.
|
|
|
105
103
|
|
|
106
104
|
- [`pytest-dotenv`](https://github.com/quiqua/pytest-dotenv)
|
|
107
105
|
- [`pytest-xdist`](https://github.com/pytest-dev/pytest-xdist)
|
|
106
|
+
- [`syrupy`](https://github.com/tophat/syrupy)
|
|
108
107
|
|
|
109
108
|
## Installation
|
|
110
109
|
|
|
@@ -114,7 +113,7 @@ Python3.8+, dependency-free, but pairs well with `pytest` and select plugins, e.
|
|
|
114
113
|
pip install modflow-devtools
|
|
115
114
|
```
|
|
116
115
|
|
|
117
|
-
Pytest, pytest plugins, and other
|
|
116
|
+
Pytest, pytest plugins, and other testing-related dependencies can be installed with:
|
|
118
117
|
|
|
119
118
|
```shell
|
|
120
119
|
pip install "modflow-devtools[test]"
|
|
@@ -122,7 +121,7 @@ pip install "modflow-devtools[test]"
|
|
|
122
121
|
|
|
123
122
|
To install from source and set up a development environment please see the [developer documentation](DEVELOPER.md).
|
|
124
123
|
|
|
125
|
-
To import `pytest` fixtures in a project consuming `modflow-devtools`, add the following to a `conftest.py` file:
|
|
124
|
+
To import `pytest` fixtures in a project consuming `modflow-devtools`, add the following to a test file or `conftest.py` file:
|
|
126
125
|
|
|
127
126
|
```python
|
|
128
127
|
pytest_plugins = [ "modflow_devtools.fixtures" ]
|
|
@@ -5,11 +5,7 @@ sphinx-rtd-theme
|
|
|
5
5
|
myst-parser
|
|
6
6
|
|
|
7
7
|
[lint]
|
|
8
|
-
|
|
9
|
-
cffconvert
|
|
10
|
-
flake8
|
|
11
|
-
isort
|
|
12
|
-
pylint
|
|
8
|
+
ruff
|
|
13
9
|
|
|
14
10
|
[test]
|
|
15
11
|
modflow-devtools[lint]
|
|
@@ -19,8 +15,10 @@ filelock
|
|
|
19
15
|
meson!=0.63.0
|
|
20
16
|
ninja
|
|
21
17
|
numpy
|
|
22
|
-
|
|
18
|
+
pandas
|
|
19
|
+
pytest!=8.1.0
|
|
23
20
|
pytest-cov
|
|
24
21
|
pytest-dotenv
|
|
25
22
|
pytest-xdist
|
|
26
23
|
PyYaml
|
|
24
|
+
syrupy
|
|
@@ -44,11 +44,7 @@ dynamic = ["version"]
|
|
|
44
44
|
|
|
45
45
|
[project.optional-dependencies]
|
|
46
46
|
lint = [
|
|
47
|
-
"
|
|
48
|
-
"cffconvert",
|
|
49
|
-
"flake8",
|
|
50
|
-
"isort",
|
|
51
|
-
"pylint"
|
|
47
|
+
"ruff"
|
|
52
48
|
]
|
|
53
49
|
test = [
|
|
54
50
|
"modflow-devtools[lint]",
|
|
@@ -58,11 +54,13 @@ test = [
|
|
|
58
54
|
"meson!=0.63.0",
|
|
59
55
|
"ninja",
|
|
60
56
|
"numpy",
|
|
61
|
-
"
|
|
57
|
+
"pandas",
|
|
58
|
+
"pytest!=8.1.0",
|
|
62
59
|
"pytest-cov",
|
|
63
60
|
"pytest-dotenv",
|
|
64
61
|
"pytest-xdist",
|
|
65
|
-
"PyYaml"
|
|
62
|
+
"PyYaml",
|
|
63
|
+
"syrupy"
|
|
66
64
|
]
|
|
67
65
|
docs = [
|
|
68
66
|
"sphinx",
|
|
@@ -75,19 +73,19 @@ docs = [
|
|
|
75
73
|
"Bug Tracker" = "https://github.com/MODFLOW-USGS/modflow-devtools/issues"
|
|
76
74
|
"Source Code" = "https://github.com/MODFLOW-USGS/modflow-devtools"
|
|
77
75
|
|
|
76
|
+
[tool.ruff]
|
|
77
|
+
target-version = "py38"
|
|
78
|
+
include = [
|
|
79
|
+
"pyproject.toml",
|
|
80
|
+
"modflow_devtools/**/*.py",
|
|
81
|
+
"autotest/**/*.py",
|
|
82
|
+
"docs/**/*.py",
|
|
83
|
+
"scripts/**/*.py",
|
|
84
|
+
".github/**/*.py",
|
|
85
|
+
]
|
|
78
86
|
|
|
79
|
-
[tool.
|
|
80
|
-
|
|
81
|
-
target_version = ["py37"]
|
|
82
|
-
|
|
83
|
-
[tool.flynt]
|
|
84
|
-
line-length = 79
|
|
85
|
-
verbose = true
|
|
86
|
-
|
|
87
|
-
[tool.isort]
|
|
88
|
-
profile = "black"
|
|
89
|
-
src_paths = ["modflow_devtools"]
|
|
90
|
-
line_length = 79
|
|
87
|
+
[tool.ruff.lint]
|
|
88
|
+
select = ["F", "E", "I001"]
|
|
91
89
|
|
|
92
90
|
[tool.setuptools]
|
|
93
91
|
packages = ["modflow_devtools"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.6.0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.4.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|