Skip to content

Commit 65a1446

Browse files
authored
refactor: define the program database in toml (#324)
* refactor: define the program database in toml usgsprograms.txt was a fixed width comma separated file with a header row, which could not carry a comment describing what a field means and had to be re-aligned by hand when a value grew. The database is now usgsprograms.toml, where each target is a table, so a field can be documented and added. standard_switch and double_switch are standard_precision and double_precision, since a switch does not say what it selects. The other field names are unchanged, srcdir in particular, which is what the rest of pymake calls a source directory. A target name that contains a dot, such as mfnwt1.1.4, is quoted, since a dot separates tables in toml and the target would otherwise be read as three nested tables. toml is read with tomllib, which is in the standard library from python 3.11, and with tomli before that. * refactor: remove the string to boolean conversion toml does not need * style: mark the toml reader fallback for the analysis tools tomli is only imported on python 3.10, which the static analysis and the coverage report do not run, so the import is marked as one they do not report on. * style: name the module constants in usgsprograms for what they are program_data_file and target_keys are constants and are named for constants, which the static analysis reported when the file the database is read from changed. The import of the toml reader is no longer marked, since the analysis does not report it.
1 parent 2cadfa0 commit 65a1446

7 files changed

Lines changed: 335 additions & 129 deletions

File tree

pixi.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pytest-xdist = "*"
2929
python = "3.11.*"
3030
requests = "*"
3131
ruff = "*"
32+
tomli = "*"
3233

3334
[tasks]
3435
postinstall = "pip install --no-build-isolation --no-deps --disable-pip-version-check -e ."

pymake/pymake_build_apps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Build MODFLOW-based models and other utility software.
22
3-
Targets are defined in the usgsprograms database (usgsprograms.txt), which can
3+
Targets are defined in the usgsprograms database (usgsprograms.toml), which can
44
be queried using functions in the usgsprograms module. An example of using
55
:code:`pymake.build_apps()` to build MODFLOW 6 is:
66

pymake/utils/usgsprograms.py

Lines changed: 27 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,8 @@
1212
6. Functions to load, update, and export a USGS-style "code.json" json file
1313
containing information in the USGS application database
1414
15-
A table listing the available pymake targets is included below:
16-
17-
.. csv-table:: Available pymake targets
18-
:file: ./usgsprograms.txt
19-
:widths: 10, 10, 10, 20, 10, 10, 10, 10, 10
20-
:header-rows: 1
15+
The available pymake targets are defined in ``usgsprograms.toml``, which is
16+
in this directory, and are listed by :code:`usgs_program_data.list_targets()`.
2117
2218
"""
2319

@@ -26,6 +22,13 @@
2622
import os
2723
import sys
2824
import warnings
25+
26+
# tomllib is in the standard library from python 3.11, and tomli is the same
27+
# reader for python 3.10, which is still supported
28+
try:
29+
import tomllib
30+
except ModuleNotFoundError:
31+
import tomli as tomllib
2932
from pathlib import Path
3033

3134
from .download import _request_header, zip_all
@@ -77,43 +80,22 @@ class dotdict(dict):
7780

7881

7982
# data file containing the USGS program data
80-
program_data_file = "usgsprograms.txt"
83+
PROGRAM_DATA_FILE = "usgsprograms.toml"
8184

8285
# keys to create for each target
83-
target_keys = (
86+
TARGET_KEYS = (
8487
"version",
8588
"current",
8689
"url",
8790
"dirname",
8891
"srcdir",
89-
"standard_switch",
90-
"double_switch",
92+
"standard_precision",
93+
"double_precision",
9194
"shared_object",
9295
"url_download_asset_date",
9396
)
9497

9598

96-
def _str_to_bool(s):
97-
"""Convert "True" and "False" strings to a boolean.
98-
99-
Parameters
100-
----------
101-
s : str
102-
String representation of boolean
103-
104-
Returns
105-
-------
106-
107-
"""
108-
if s == "True":
109-
return True
110-
elif s == "False":
111-
return False
112-
else:
113-
msg = f'Invalid string passed - "{s}"'
114-
raise ValueError(msg)
115-
116-
11799
class usgs_program_data:
118100
"""USGS program database class."""
119101

@@ -130,35 +112,18 @@ def _build_usgs_database(self):
130112
"""
131113
# pth = os.path.dirname(os.path.abspath(pymake.__file__))
132114
pth = os.path.dirname(os.path.abspath(__file__))
133-
fpth = os.path.join(pth, program_data_file)
134-
url_in = open(fpth, "r").read().split("\n")
115+
fpth = os.path.join(pth, PROGRAM_DATA_FILE)
116+
with open(fpth, "rb") as f:
117+
programs = tomllib.load(f)["program"]
135118

136119
program_data = {}
137-
for line in url_in[1:]:
138-
# skip blank lines
139-
if len(line.strip()) < 1:
140-
continue
141-
# parse comma separated line
142-
t = [item.strip() for item in line.split(sep=",")]
143-
# programmatically build a dictionary for each target
144-
d = {}
145-
for idx, key in enumerate(target_keys):
146-
if key in ("url_download_asset_date",):
147-
value = None
148-
else:
149-
value = t[idx + 1]
150-
if key in (
151-
"current",
152-
"standard_switch",
153-
"double_switch",
154-
"shared_object",
155-
):
156-
value = _str_to_bool(value)
157-
d[key] = value
120+
for target, entry in programs.items():
121+
# programmatically build a dictionary for each target, so that a
122+
# target has every key whether the file defines it or not
123+
d = {key: entry.get(key) for key in TARGET_KEYS}
158124

159125
# make it possible to access each key with a dot (.)
160-
d = dotdict(d)
161-
program_data[t[0]] = d
126+
program_data[target] = dotdict(d)
162127

163128
return dotdict(program_data)
164129

@@ -287,9 +252,9 @@ def get_precision(key):
287252
"""
288253
target = usgs_program_data().get_target(key)
289254
precision = []
290-
if target.standard_switch:
255+
if target.standard_precision:
291256
precision.append("default")
292-
if target.double_switch:
257+
if target.double_precision:
293258
precision.append("double")
294259
return precision
295260

@@ -393,7 +358,7 @@ def export_json(
393358
sel = "the current"
394359
print(
395360
f'writing a json file ("{fpth}") of {sel} USGS programs\n'
396-
f'in the "{program_data_file}" database.\n'
361+
f'in the "{PROGRAM_DATA_FILE}" database.\n'
397362
)
398363
if prog_data is not None:
399364
for idx, key in enumerate(prog_data.keys()):
@@ -477,13 +442,13 @@ def export_json(
477442
for target in pop_list:
478443
del prog_data[target]
479444

480-
# update double_switch based on executables in appdir
445+
# update double_precision based on executables in appdir
481446
for appdir_file in appdir.iterdir():
482447
temp_target = appdir_file.stem
483448
if temp_target.endswith("dbl"):
484449
temp_target = temp_target.replace("dbl", "")
485450
if temp_target in prog_data.keys():
486-
prog_data[temp_target]["double_switch"] = True
451+
prog_data[temp_target]["double_precision"] = True
487452

488453
# write code.json to root directory - used by executables CI
489454
with open(file_name, "w") as file_obj:
@@ -554,7 +519,7 @@ def load_json(fpth="code.json"):
554519
for key, value in json_dict.items():
555520
try:
556521
for kk in value.keys():
557-
if kk not in target_keys:
522+
if kk not in TARGET_KEYS:
558523
raise KeyError(msg + f' - key ("{kk}")')
559524
except:
560525
raise KeyError(msg)

0 commit comments

Comments
 (0)