Skip to content

Commit df5dd9b

Browse files
AnsMelaniePProfizi
andauthored
feat: support "update" files for operator descriptions during operator documentation generation (#2533)
Co-authored-by: Paul Profizi <[email protected]>
1 parent c075b18 commit df5dd9b

File tree

3 files changed

+124
-14
lines changed

3 files changed

+124
-14
lines changed

.github/workflows/test_docker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ jobs:
208208
- name: "Test Documentation"
209209
uses: nick-fields/retry@v3
210210
with:
211-
timeout_minutes: 4
211+
timeout_minutes: 8
212212
max_attempts: 2
213213
shell: bash
214214
command: |

src/ansys/dpf/core/documentation/generate_operators_doc.py

Lines changed: 108 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
from __future__ import annotations
2525

2626
import argparse
27-
from os import PathLike
27+
import os
2828
from pathlib import Path
29+
import re
2930

3031
from ansys.dpf import core as dpf
3132
from ansys.dpf.core.changelog import Changelog
@@ -51,7 +52,7 @@ def __init__(
5152

5253

5354
def initialize_server(
54-
ansys_path: str | PathLike = None,
55+
ansys_path: str | os.PathLike = None,
5556
include_composites: bool = False,
5657
include_sound: bool = False,
5758
verbose: bool = False,
@@ -105,6 +106,98 @@ def initialize_server(
105106
return server
106107

107108

109+
def extract_operator_description_update(content: str) -> str:
110+
"""Extract the updated description to use for an operator.
111+
112+
Parameters
113+
----------
114+
content:
115+
The contents of the '*_upd.md' file.
116+
117+
Returns
118+
-------
119+
description_update:
120+
The updated description to use for the operator.
121+
"""
122+
match = re.search(r"## Description\s*(.*?)\s*(?=## |\Z)", content, re.DOTALL)
123+
return match.group(0) + os.linesep if match else None
124+
125+
126+
def replace_operator_description(original_documentation: str, new_description: str):
127+
"""Replace the original operator description with a new one in the operator documentation file.
128+
129+
Parameters
130+
----------
131+
original_documentation:
132+
Original operator documentation.
133+
new_description:
134+
New operator description
135+
136+
Returns
137+
-------
138+
updated_documentation:
139+
The updated operator documentation content
140+
141+
"""
142+
return re.sub(
143+
r"## Description\s*.*?(?=## |\Z)", new_description, original_documentation, flags=re.DOTALL
144+
)
145+
146+
147+
def update_operator_descriptions(
148+
docs_path: Path,
149+
verbose: bool = False,
150+
):
151+
"""Update operator descriptions based on '*_upd.md' files in DPF documentation sources.
152+
153+
Parameters
154+
----------
155+
docs_path:
156+
Root path of the DPF documentation to update operator descriptions for.
157+
verbose:
158+
Whether to print progress information.
159+
160+
"""
161+
all_md_files = {}
162+
specs_path = docs_path / Path("operator-specifications")
163+
# Walk through the target directory and all subdirectories
164+
for root, _, files in os.walk(specs_path):
165+
for file in files:
166+
if file.endswith(".md"):
167+
full_path = Path(root) / Path(file)
168+
all_md_files[str(full_path)] = file # Store full path and just filename
169+
170+
for base_path, file_name in all_md_files.items():
171+
if file_name.endswith("_upd.md"):
172+
continue # Skip update files
173+
174+
# Construct the expected update file name and path
175+
upd_file_name = f"{file_name[:-3]}_upd.md"
176+
177+
# Look for the update file in the same folder
178+
upd_path = Path(base_path).parent / Path(upd_file_name)
179+
if not upd_path.exists():
180+
continue
181+
182+
# Load contents
183+
with Path(base_path).open(mode="r", encoding="utf-8") as bf:
184+
base_content = bf.read()
185+
with Path(upd_path).open(mode="r", encoding="utf-8") as uf:
186+
upd_content = uf.read()
187+
188+
# Extract and replace description
189+
new_description = extract_operator_description_update(upd_content)
190+
if new_description:
191+
updated_content = replace_operator_description(base_content, new_description)
192+
with Path(base_path).open(mode="w", encoding="utf-8") as bf:
193+
bf.write(updated_content)
194+
if verbose:
195+
print(f"Updated description for: {file_name}")
196+
else:
197+
if verbose:
198+
print(f"No operator description found in: {upd_path}")
199+
200+
108201
def fetch_doc_info(server: dpf.AnyServerType, operator_name: str) -> dict:
109202
"""Fetch information about the specifications of a given operator.
110203
@@ -265,14 +358,14 @@ def generate_operator_doc(
265358
if not include_private and operator_info["exposure"] == "private":
266359
return
267360
template_path = Path(__file__).parent / "operator_doc_template.md"
268-
spec_folder = output_path / "operator-specifications"
361+
spec_folder = output_path / Path("operator-specifications")
269362
category_dir = spec_folder / category
270363
spec_folder.mkdir(parents=True, exist_ok=True)
271364
if category is not None:
272365
category_dir.mkdir(parents=True, exist_ok=True) # Ensure all parent directories are created
273366
file_dir = category_dir
274367
else:
275-
file_dir = output_path / "operator-specifications"
368+
file_dir = spec_folder
276369
with Path.open(template_path, "r") as file:
277370
template = jinja2.Template(file.read())
278371

@@ -291,7 +384,7 @@ def generate_toc_tree(docs_path: Path):
291384
292385
"""
293386
data = []
294-
specs_path = docs_path / "operator-specifications"
387+
specs_path = docs_path / Path("operator-specifications")
295388
for folder in specs_path.iterdir():
296389
if folder.is_dir(): # Ensure 'folder' is a directory
297390
category = folder.name
@@ -308,18 +401,19 @@ def generate_toc_tree(docs_path: Path):
308401

309402
# Render the Jinja2 template
310403
template_path = Path(__file__).parent / "toc_template.j2"
311-
with Path.open(template_path, "r") as template_file:
404+
with template_path.open(mode="r") as template_file:
312405
template = jinja2.Template(template_file.read())
313406
output = template.render(data=data) # Pass 'data' as a named argument
314407

315408
# Write the rendered output to toc.yml at the operators_doc level
316-
with Path.open(docs_path / "toc.yml", "w") as file:
409+
toc_path = docs_path / Path("toc.yml")
410+
with toc_path.open(mode="w") as file:
317411
file.write(output)
318412

319413

320414
def generate_operators_doc(
321-
ansys_path: Path,
322415
output_path: Path,
416+
ansys_path: Path = None,
323417
include_composites: bool = False,
324418
include_sound: bool = False,
325419
include_private: bool = False,
@@ -334,10 +428,10 @@ def generate_operators_doc(
334428
335429
Parameters
336430
----------
337-
ansys_path:
338-
Path to an Ansys/DPF installation.
339431
output_path:
340432
Path to write the output files at.
433+
ansys_path:
434+
Path to an Ansys/DPF installation.
341435
include_composites:
342436
Whether to include operators of the Composites plugin.
343437
include_sound:
@@ -357,7 +451,10 @@ def generate_operators_doc(
357451
operators = get_plugin_operators(server, desired_plugin)
358452
for operator_name in operators:
359453
generate_operator_doc(server, operator_name, include_private, output_path)
454+
# Generate the toc tree
360455
generate_toc_tree(output_path)
456+
# Use update files in output_path
457+
update_operator_descriptions(output_path)
361458

362459

363460
def run_with_args(): # pragma: nocover
@@ -389,8 +486,8 @@ def run_with_args(): # pragma: nocover
389486
args = parser.parse_args()
390487

391488
generate_operators_doc(
392-
ansys_path=args.ansys_path,
393489
output_path=args.output_path,
490+
ansys_path=args.ansys_path,
394491
include_composites=args.include_composites,
395492
include_sound=args.include_sound,
396493
include_private=args.include_private,

tests/test_documentation.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,17 @@ def test_generate_operators_doc(tmp_path: Path):
3434
assert file_to_test.exists()
3535

3636

37-
def test_generate_operators_doc_plugin(tmp_path: Path):
37+
def test_generate_operators_doc_plugin_and_update(tmp_path: Path):
38+
specs_path = tmp_path / "operator-specifications"
39+
specs_path.mkdir()
40+
utility_path = specs_path / "utility"
41+
utility_path.mkdir()
42+
forward_update_path = utility_path / "forward_upd.md"
43+
test_string = r"""## Description
44+
45+
Test update"""
46+
with forward_update_path.open(mode="w", encoding="utf-8") as ff:
47+
ff.write(test_string)
3848
generate_operators_doc(
3949
ansys_path=dpf.SERVER.ansys_path,
4050
output_path=tmp_path,
@@ -43,5 +53,8 @@ def test_generate_operators_doc_plugin(tmp_path: Path):
4353
)
4454
file_to_test = tmp_path / "toc.yml"
4555
assert file_to_test.exists()
46-
file_to_test = tmp_path / "operator-specifications" / "utility" / "forward.md"
56+
file_to_test = utility_path / "forward.md"
4757
assert file_to_test.exists()
58+
with file_to_test.open(mode="r", encoding="utf-8") as ff:
59+
text = ff.read()
60+
assert test_string in text

0 commit comments

Comments
 (0)