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.
Files changed (26) hide show
  1. {modflow-devtools-1.4.0/modflow_devtools.egg-info → modflow_devtools-1.6.0}/PKG-INFO +8 -9
  2. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/README.md +3 -2
  3. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/__init__.py +2 -2
  4. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/download.py +28 -39
  5. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/fixtures.py +28 -55
  6. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/imports.py +2 -4
  7. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/latex.py +56 -12
  8. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/markers.py +13 -10
  9. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/misc.py +101 -61
  10. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/ostags.py +2 -2
  11. modflow_devtools-1.6.0/modflow_devtools/snapshots.py +159 -0
  12. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0/modflow_devtools.egg-info}/PKG-INFO +8 -9
  13. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/SOURCES.txt +1 -0
  14. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/requires.txt +4 -6
  15. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/pyproject.toml +17 -19
  16. modflow_devtools-1.6.0/version.txt +1 -0
  17. modflow-devtools-1.4.0/version.txt +0 -1
  18. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/LICENSE.md +0 -0
  19. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/MANIFEST.in +0 -0
  20. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/build.py +0 -0
  21. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools/zip.py +0 -0
  22. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/dependency_links.txt +0 -0
  23. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/not-zip-safe +0 -0
  24. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/modflow_devtools.egg-info/top_level.txt +0 -0
  25. {modflow-devtools-1.4.0 → modflow_devtools-1.6.0}/setup.cfg +0 -0
  26. {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.4.0
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: black; extra == "lint"
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: pytest; extra == "test"
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 optional dependencies can be installed with:
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 optional dependencies can be installed with:
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" ]
@@ -1,6 +1,6 @@
1
1
  __author__ = "Joseph D. Hughes"
2
- __date__ = "Feb 19, 2024"
3
- __version__ = "1.4.0"
2
+ __date__ = "May 30, 2024"
3
+ __version__ = "1.6.0"
4
4
  __maintainer__ = "Joseph D. Hughes"
5
5
  __email__ = "jdhughes@usgs.gov"
6
6
  __status__ = "Production"
@@ -58,10 +58,10 @@ def get_releases(
58
58
  """
59
59
 
60
60
  if "/" not in repo:
61
- raise ValueError(f"repo format must be owner/name")
61
+ raise ValueError("repo format must be owner/name")
62
62
 
63
63
  if not isinstance(retries, int) or retries < 1:
64
- raise ValueError(f"retries must be a positive int")
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 repo {repo} (page {page}, {per_page} per page)"
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(f"repo format must be owner/name")
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(f"tag must be a non-empty string")
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(f"retries must be a positive int")
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(f"repo format must be owner/name")
213
+ raise ValueError("repo format must be owner/name")
213
214
 
214
215
  if not isinstance(retries, int) or retries < 1:
215
- raise ValueError(f"retries must be a positive int")
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(f"repo format must be owner/name")
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(f"tag must be a non-empty string")
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(f"retries must be a positive int")
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 about an artifact as returned
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(f"repo format must be owner/name")
296
+ raise ValueError("repo format must be owner/name")
296
297
 
297
298
  if not isinstance(retries, int) or retries < 1:
298
- raise ValueError(f"retries must be a positive int")
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(f"name must be a non-empty string")
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(f"repo format must be owner/name")
387
+ raise ValueError("repo format must be owner/name")
391
388
 
392
389
  if not isinstance(retries, int) or retries < 1:
393
- raise ValueError(f"retries must be a positive int")
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 use_pandas(request):
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 correspondingly named subdirectories at the given "
123
- "location after tests complete. This option can be used to exclude test results from automatic cleanup, "
124
- "e.g. for manual inspection. The provided path is created if it does not already exist. An error is "
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 correspondingly named subdirectories at the given "
133
- "location if the test case fails. This option automatically saves the outputs of failed tests in the "
134
- "given location. The path is created if it doesn't already exist. An error is thrown if files with the "
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., to test framework or fixtures).",
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. Select 'raw', 'recarray', or 'dataframe'.",
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=[name for name, ex in example_scenarios.items()],
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 os
1
+ from os import PathLike
2
+ from pathlib import Path
3
+ from typing import Iterable, Union
2
4
 
3
5
 
4
- def build_table(caption, fpth, arr, headings=None, col_widths=None):
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
- if not fpth.endswith(".tex"):
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, label, headings, col_widths=None, center=True, firsthead=False
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(f"Version must a string")
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 given OS (as returned by sys.platform)
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 or model defined in the given namefile.
154
- The namefile may be for an entire simulation or for a GWF or GWT model. If a simulation
155
- namefile is given, packages used in its component model namefiles will be included.
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 = [l for l in lines if l.strip().lower().startswith("gwf6 ")]
171
- gwt_lines = [l for l in lines if l.strip().lower().startswith("gwt6 ")]
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
- f"Failed to parse GWF or GWT model namefile from simulation namefile line: {line}"
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
- packages + get_packages(parse_model_namefile(line)) + ["gwt"]
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
- ll = line.strip().split()
197
- if len(ll) < 2:
197
+ line = line.strip().split()
198
+ if len(line) < 2:
198
199
  continue
199
200
 
200
- l = ll[0].lower()
201
- if any(l.startswith(c) for c in ["#", "!", "data", "list"]) or l in [
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
- l = l.replace("6", "")
210
+ line = line.replace("6", "")
210
211
 
211
- packages.append(l.lower())
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
- namefile_paths = get_namefile_paths(
299
- path, prefix, namefile, excluded, selected, packages
300
- )
301
- model_paths = sorted(
302
- list(set([p.parent for p in namefile_paths if p.parent.name]))
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 (or None if the connection fails).
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 remaining before rate-limiting"
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(pkg: str, strict: bool = False) -> bool:
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 try_import():
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(pkg)
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
- found = False
409
- if not strict:
410
- found = pkg in _has_pkg_cache or try_metadata()
411
- if not found:
412
- found = try_import()
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.4.0
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: black; extra == "lint"
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: pytest; extra == "test"
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 optional dependencies can be installed with:
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" ]
@@ -13,6 +13,7 @@ modflow_devtools/latex.py
13
13
  modflow_devtools/markers.py
14
14
  modflow_devtools/misc.py
15
15
  modflow_devtools/ostags.py
16
+ modflow_devtools/snapshots.py
16
17
  modflow_devtools/zip.py
17
18
  modflow_devtools.egg-info/PKG-INFO
18
19
  modflow_devtools.egg-info/SOURCES.txt
@@ -5,11 +5,7 @@ sphinx-rtd-theme
5
5
  myst-parser
6
6
 
7
7
  [lint]
8
- black
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
- pytest
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
- "black",
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
- "pytest",
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.black]
80
- line-length = 79
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