Skip to content

Commit 4cd95eb

Browse files
authored
ci(release): generate release notes latex from toml (#2108)
Store release notes in a TOML file and build the develop.tex file from it at distribution time. Storing release notes in a structured form allows more easily using them elsewhere, e.g. we could substitute in the nightly build release descriptions (#2059) or into the RTD site. Add tomli and Jinja2 to the Python dependencies. I figure we might as well adopt Jinja for this since we have discussed adopting it for other tasks here as in flopy sooner or later. Also move the contents of develop.tex into previous/v6.6.1.tex, we hadn't done this post-6.6.1 step yet. As this is one of the only remaining manual steps at release time we can automate it at some point? By way of demonstration, the new develop.toml representing the develop branch right now becomes [sections] features = "NEW FUNCTIONALITY" fixes = "BUG FIXES AND OTHER CHANGES TO EXISTING FUNCTIONALITY" examples = "EXAMPLES" [subsections] basic = "BASIC FUNCTIONALITY" internal = "INTERNAL FLOW PACKAGES" stress = "STRESS PACKAGES" advanced = "ADVANCED STRESS PACKAGES" solution = "SOLUTION" exchanges = "EXCHANGES" parallel = "PARALLEL" [[items]] section = "features" description = "The binary grid file's name may now be specified in all discretization packages with option GRB6 FILEOUT followed by a file path. If this option is not provided, the binary grid file will be named as before, identical to the discretization file name plus a '.grb' extension. Note that renaming the binary grid file may break downstream integrations which expect the default name." Sections and subsections can be added/removed to the leading tables as necessary, then referenced from items. For now all the existing headers/subheaders which were previously in the template are included. I considered different ways of doing this, including - structuring the TOML exactly as the latex was/will be, with a list of items inside each nested section — but I figured a single flat list of items was clearer - putting sections/subsections in the jinja template and removing them from the toml file, then using a "tag" system to assign an item to section/subsection ..and went with the current approach for lack of a strong sense yet which is best.
1 parent 131b6d9 commit 4cd95eb

File tree

9 files changed

+188
-58
lines changed

9 files changed

+188
-58
lines changed

distribution/build_dist.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ def build_distribution(
324324
build_documentation(
325325
bin_path=output_path / "bin",
326326
full=full,
327-
output_path=output_path / "doc",
327+
out_path=output_path / "doc",
328328
force=force,
329329
)
330330

distribution/build_docs.py

Lines changed: 78 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,40 @@ def build_deprecations_tex(force: bool = False):
128128
out, err, ret = run_py_script("mk_deprecations.py", md_path, verbose=True)
129129
assert not ret, out + err
130130

131-
# check deprecations files exist
132131
assert md_path.is_file()
133132
assert tex_path.is_file()
134133

135134

135+
def build_notes_tex(force: bool = False):
136+
"""Build LaTeX files for the release notes."""
137+
138+
build_deprecations_tex(force=force)
139+
140+
toml_path = RELEASE_NOTES_PATH / "develop.toml"
141+
tex_path = RELEASE_NOTES_PATH / "develop.tex"
142+
if tex_path.is_file() and not force:
143+
print(f"{tex_path} already exists.")
144+
else:
145+
tex_path.unlink(missing_ok=True)
146+
with set_dir(RELEASE_NOTES_PATH):
147+
out, err, ret = run_py_script(
148+
"mk_releasenotes.py", toml_path, tex_path, verbose=True
149+
)
150+
assert not ret, out + err
151+
152+
assert tex_path.is_file()
153+
154+
136155
@no_parallel
137156
def test_build_deprecations_tex():
138157
build_deprecations_tex(force=True)
139158

140159

160+
@no_parallel
161+
def test_build_notes_tex():
162+
build_notes_tex(force=True)
163+
164+
141165
def build_mf6io_tex(models: Optional[list[str]] = None, force: bool = False):
142166
"""Build LaTeX files for the MF6IO guide from DFN files."""
143167

@@ -183,7 +207,7 @@ def test_build_mf6io_tex():
183207
build_mf6io_tex(force=True)
184208

185209

186-
def build_usage_example_tex(
210+
def build_usage_tex(
187211
workspace_path: PathLike, bin_path: PathLike, example_model_path: PathLike
188212
):
189213
"""
@@ -320,13 +344,38 @@ def test_build_pdfs_from_tex(tmp_path):
320344
]
321345

322346
build_pdfs(tex_paths, tmp_path)
323-
for p in tex_paths[:-1] + bbl_paths:
324-
assert p.is_file()
347+
348+
expected_paths = tex_paths[:-1] + bbl_paths
349+
assert all(p.is_file() for p in expected_paths)
350+
351+
352+
def fetch_example_docs(
353+
out_path: PathLike, force: bool = False, repo_owner: str = "MODFLOW-USGS"
354+
):
355+
pdf_name = "mf6examples.pdf"
356+
if force or not (out_path / pdf_name).is_file():
357+
latest = get_release(f"{repo_owner}/modflow6-examples", "latest")
358+
assets = latest["assets"]
359+
asset = next(iter([a for a in assets if a["name"] == pdf_name]), None)
360+
download_and_unzip(asset["browser_download_url"], out_path, verbose=True)
361+
362+
363+
def fetch_usgs_pubs(out_path: PathLike, force: bool = False):
364+
for url in PUB_URLS:
365+
print(f"Downloading publication: {url}")
366+
try:
367+
download_and_unzip(url, path=out_path, delete_zip=False)
368+
assert (out_path / url.rpartition("/")[2]).is_file()
369+
except HTTPError as e:
370+
if "404" in str(e):
371+
warn(f"Publication not found: {url}")
372+
else:
373+
raise
325374

326375

327376
def build_documentation(
328377
bin_path: PathLike,
329-
output_path: PathLike,
378+
out_path: PathLike,
330379
force: bool = False,
331380
full: bool = False,
332381
models: Optional[list[str]] = None,
@@ -337,72 +386,46 @@ def build_documentation(
337386
print(f"Building {'full' if full else 'minimal'} documentation")
338387

339388
bin_path = Path(bin_path).expanduser().absolute()
340-
output_path = Path(output_path).expanduser().absolute()
389+
out_path = Path(out_path).expanduser().absolute()
390+
pdf_path = out_path / "mf6io.pdf"
341391

342-
if (output_path / "mf6io.pdf").is_file() and not force:
343-
print(f"{output_path / 'mf6io.pdf'} already exists")
392+
if not force and pdf_path.is_file():
393+
print(f"{pdf_path} already exists, nothing to do")
344394
return
345395

346-
# make sure output directory exists
347-
output_path.mkdir(parents=True, exist_ok=True)
348-
349-
# build LaTex input/output docs from DFN files
350-
build_mf6io_tex(force=force, models=models)
396+
out_path.mkdir(parents=True, exist_ok=True)
351397

352-
# build LaTeX input/output example model docs
353398
with TemporaryDirectory() as temp:
354-
build_usage_example_tex(
399+
build_mf6io_tex(force=force, models=models)
400+
build_usage_tex(
355401
bin_path=bin_path,
356402
workspace_path=Path(temp),
357403
example_model_path=PROJ_ROOT_PATH / ".mf6minsim",
358404
)
405+
build_notes_tex(force=force)
359406

360-
# build deprecations table LaTeX
361-
build_deprecations_tex(force=force)
407+
if full:
408+
build_benchmark_tex(out_path=out_path, force=force, repo_owner=repo_owner)
409+
fetch_example_docs(out_path=out_path, force=force, repo_owner=repo_owner)
410+
fetch_usgs_pubs(out_path=out_path, force=force)
411+
tex_paths = TEX_PATHS["full"]
412+
else:
413+
tex_paths = TEX_PATHS["minimal"]
362414

363-
if full:
364-
# build benchmarks table LaTex, running benchmarks first if necessary
365-
build_benchmark_tex(output_path=output_path, force=force)
366-
367-
# download example docs
368-
pdf_name = "mf6examples.pdf"
369-
if force or not (output_path / pdf_name).is_file():
370-
latest = get_release(f"{repo_owner}/modflow6-examples", "latest")
371-
assets = latest["assets"]
372-
asset = next(iter([a for a in assets if a["name"] == pdf_name]), None)
373-
download_and_unzip(asset["browser_download_url"], output_path, verbose=True)
374-
375-
# download publications
376-
for url in PUB_URLS:
377-
print(f"Downloading publication: {url}")
378-
try:
379-
download_and_unzip(url, path=output_path, delete_zip=False)
380-
assert (output_path / url.rpartition("/")[2]).is_file()
381-
except HTTPError as e:
382-
if "404" in str(e):
383-
warn(f"Publication not found: {url}")
384-
else:
385-
raise
386-
387-
# convert LaTex to PDF
388-
build_pdfs(tex_paths=TEX_PATHS["full"], output_path=output_path, force=force)
389-
else:
390-
# just convert LaTeX to PDF
391-
build_pdfs(tex_paths=TEX_PATHS["minimal"], output_path=output_path, force=force)
415+
build_pdfs(tex_paths=tex_paths, output_path=out_path, force=force)
392416

393417
# enforce os line endings on all text files
394418
windows_line_endings = True
395-
convert_line_endings(output_path, windows_line_endings)
419+
convert_line_endings(out_path, windows_line_endings)
396420

397421
# make sure we have expected PDFs
398-
assert (output_path / "mf6io.pdf").is_file()
422+
assert pdf_path.is_file()
399423
if full:
400-
assert (output_path / "mf6io.pdf").is_file()
401-
assert (output_path / "ReleaseNotes.pdf").is_file()
402-
assert (output_path / "zonebudget.pdf").is_file()
403-
assert (output_path / "converter_mf5to6.pdf").is_file()
404-
assert (output_path / "mf6suptechinfo.pdf").is_file()
405-
assert (output_path / "mf6examples.pdf").is_file()
424+
assert (out_path / "ReleaseNotes.pdf").is_file()
425+
assert (out_path / "zonebudget.pdf").is_file()
426+
assert (out_path / "converter_mf5to6.pdf").is_file()
427+
assert (out_path / "mf6suptechinfo.pdf").is_file()
428+
assert (out_path / "mf6examples.pdf").is_file()
406429

407430

408431
@no_parallel
@@ -477,7 +500,7 @@ def test_build_documentation(tmp_path):
477500
models = args.model if args.model else DEFAULT_MODELS
478501
build_documentation(
479502
bin_path=bin_path,
480-
output_path=output_path,
503+
out_path=output_path,
481504
force=args.force,
482505
full=args.full,
483506
models=models,

doc/ReleaseNotes/develop.tex.jinja

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
\subsection{Version mf(( version ))---(( date ))}
2+
3+
([ for section_key, section_items in items|groupby("section") ])
4+
\textbf{\underline{(( sections[section_key] ))}}
5+
6+
([ for subsection_key, subsection_items in section_items|groupby("subsection") ])
7+
([ set subsection = subsections[subsection_key] ])
8+
([ if subsection|length > 0 ])
9+
\underline{(( subsection ))}
10+
([ endif ])
11+
12+
\begin{itemize}
13+
([ for item in subsection_items ])
14+
\item (( item.description ))
15+
([ endfor ])
16+
\end{itemize}
17+
18+
([ endfor ])
19+
([ endfor ])

doc/ReleaseNotes/develop.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[sections]
2+
features = "NEW FUNCTIONALITY"
3+
fixes = "BUG FIXES AND OTHER CHANGES TO EXISTING FUNCTIONALITY"
4+
examples = "EXAMPLES"
5+
6+
[subsections]
7+
basic = "BASIC FUNCTIONALITY"
8+
internal = "INTERNAL FLOW PACKAGES"
9+
stress = "STRESS PACKAGES"
10+
advanced = "ADVANCED STRESS PACKAGES"
11+
solution = "SOLUTION"
12+
exchanges = "EXCHANGES"
13+
parallel = "PARALLEL"
14+
15+
[[items]]
16+
section = "features"
17+
subsection = "basic"
18+
description = "The binary grid file's name may now be specified in all discretization packages with option GRB6 FILEOUT followed by a file path. If this option is not provided, the binary grid file will be named as before, identical to the discretization file name plus a ``.grb'' extension. Note that renaming the binary grid file may break downstream integrations which expect the default name."

doc/ReleaseNotes/mk_deprecations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,4 @@
6868
ftex.close()
6969
print(f"Created LaTex file {fnametex} from markdown deprecations file {fpath}")
7070
else:
71-
warn(f"Deprecations not found: {fpath}")
71+
warn(f"Deprecations file not found: {fpath}")
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# This script converts the release notes TOML file
2+
# to a latex file, from which is later built a PDF.
3+
import argparse
4+
import datetime
5+
import sys
6+
from pathlib import Path
7+
from warnings import warn
8+
9+
version_file = Path(__file__).parents[2] / "version.txt"
10+
version = version_file.read_text().strip()
11+
date = datetime.date.today().strftime("%b %d, %Y")
12+
13+
14+
if __name__ == "__main__":
15+
parser = argparse.ArgumentParser()
16+
parser.add_argument("toml_path")
17+
parser.add_argument("tex_path")
18+
args = parser.parse_args()
19+
toml_path = Path(args.toml_path).expanduser().absolute()
20+
tex_path = Path(args.tex_path).expanduser().absolute()
21+
if not toml_path.is_file():
22+
warn(f"Release notes TOML file not found: {toml_path}")
23+
sys.exit(0)
24+
25+
tex_path.unlink(missing_ok=True)
26+
27+
import tomli
28+
from jinja2 import Environment, FileSystemLoader
29+
30+
loader = FileSystemLoader(Path(__file__).parent)
31+
env = Environment(
32+
loader=loader,
33+
trim_blocks=True,
34+
lstrip_blocks=True,
35+
line_statement_prefix="_",
36+
keep_trailing_newline=True,
37+
# since latex uses curly brackets,
38+
# replace block/var start/end tags
39+
block_start_string="([",
40+
block_end_string="])",
41+
variable_start_string="((",
42+
variable_end_string="))",
43+
)
44+
template = env.get_template(f"{tex_path.name}.jinja")
45+
with open(tex_path, "w") as tex_file:
46+
with open(toml_path, "rb") as toml_file:
47+
content = tomli.load(toml_file)
48+
sections = content.get("sections", [])
49+
subsections = content.get("subsections", [])
50+
items = content.get("items", [])
51+
# make sure each item has a subsection entry even if empty
52+
for item in items:
53+
if not item.get("subsection"):
54+
item["subsection"] = ""
55+
if not any(items):
56+
warn("No release notes found, aborting")
57+
sys.exit(0)
58+
tex_file.write(
59+
template.render(
60+
sections=sections,
61+
subsections=subsections,
62+
items=items,
63+
version=version,
64+
date=date,
65+
)
66+
)

doc/ReleaseNotes/develop.tex renamed to doc/ReleaseNotes/previous/v6.6.1.tex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
% Use this template for starting initializing the release notes
22
% after a release has just been made.
33

4-
%\item \currentmodflowversion
54
\subsection{Version mf6.6.1---February 7, 2025}
65

76
\underline{NEW FUNCTIONALITY}

environment.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
- fprettify
1515
- fortran-language-server
1616
- gitpython
17+
- Jinja2>=3.1.5,<4
1718
- jupytext
1819
- matplotlib
1920
- meson=1.3.0
@@ -40,4 +41,5 @@ dependencies:
4041
- scipy
4142
- shapely
4243
- syrupy
44+
- tomli>=2.2.1,<3
4345

pixi.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ flaky = "*"
1313
fortran-language-server = "*"
1414
fprettify = "*"
1515
gitpython = "*"
16+
jinja2 = ">=3.1.5,<4"
1617
jupytext = "*"
1718
matplotlib = "*"
1819
meson = "==1.3.0"
@@ -37,8 +38,10 @@ ruff = "*"
3738
scipy = "*"
3839
shapely = "*"
3940
syrupy = "*"
41+
tomli = ">=2.2.1,<3"
4042
xmipy = "*"
4143

44+
4245
[feature.rtd.dependencies]
4346
numpy = "*"
4447
bmipy = "*"

0 commit comments

Comments
 (0)