Skip to content

Commit 51c6bce

Browse files
committed
Create ansys.dpf.core.documentation module
1 parent f5cf73b commit 51c6bce

File tree

4 files changed

+208
-48
lines changed

4 files changed

+208
-48
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright (C) 2020 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
"""Documentation generation tools."""

.ci/generate_operators_doc.py renamed to src/ansys/dpf/core/documentation/generate_operators_doc.py

Lines changed: 186 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,75 @@
1+
# Copyright (C) 2020 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
"""Generation of Markdown documentation source files for operators of a given DPF installation."""
23+
24+
from __future__ import annotations
25+
126
import argparse
27+
from os import PathLike
228
from pathlib import Path
329

4-
from jinja2 import Template
5-
630
from ansys.dpf import core as dpf
731
from ansys.dpf.core.changelog import Changelog
832
from ansys.dpf.core.core import load_library
933
from ansys.dpf.core.dpf_operator import available_operator_names
1034

1135

12-
def initialize_server(ansys_path=None, include_composites=False, include_sound=False):
36+
class Jinja2ImportError(ModuleNotFoundError):
37+
"""Error raised when Jinja2 could not be imported during operator documentation generation."""
38+
39+
def __init__(
40+
self,
41+
msg="To generate Markdown documentation of operators, please install jinja2 with:\n"
42+
"pip install jinja2",
43+
):
44+
ModuleNotFoundError.__init__(self, msg)
45+
46+
47+
try:
48+
import jinja2
49+
except ModuleNotFoundError:
50+
raise Jinja2ImportError
51+
52+
53+
def initialize_server(
54+
ansys_path: str | PathLike = None, include_composites: bool = False, include_sound: bool = False
55+
) -> dpf.AnyServerType:
56+
"""Initialize a DPF server for a given installation folder by loading required plugins.
57+
58+
Parameters
59+
----------
60+
ansys_path:
61+
Path to the DPF installation to use to start a server.
62+
include_composites:
63+
Whether to generate documentation for operators of the Composites plugin.
64+
include_sound:
65+
Whether to generate documentation for operators of the Sound DPF plugin.
66+
67+
Returns
68+
-------
69+
server:
70+
A running DPF server to generate operator documentation for.
71+
72+
"""
1373
server = dpf.start_local_server(ansys_path=ansys_path)
1474
print(server.plugins)
1575
print(f"Ansys Path: {server.ansys_path}")
@@ -37,20 +97,35 @@ def initialize_server(ansys_path=None, include_composites=False, include_sound=F
3797
return server
3898

3999

40-
def fetch_doc_info(server, operator_name):
100+
def fetch_doc_info(server: dpf.AnyServerType, operator_name: str) -> dict:
101+
"""Fetch information about the specifications of a given operator.
102+
103+
Parameters
104+
----------
105+
server:
106+
A DPF server to query the specifications of the operator.
107+
operator_name:
108+
The name of the operator of interest.
109+
110+
Returns
111+
-------
112+
doc_info:
113+
Information about the operator structured for use with the documentation template.
114+
115+
"""
41116
spec = dpf.Operator.operator_specification(op_name=operator_name, server=server)
42117
input_info = []
43118
output_info = []
44119
configurations_info = []
45120
for input_pin in spec.inputs:
46-
input = spec.inputs[input_pin]
121+
input_pin_info = spec.inputs[input_pin]
47122
input_info.append(
48123
{
49124
"pin_number": input_pin,
50-
"name": input.name,
51-
"types": [str(t) for t in input._type_names],
52-
"document": input.document,
53-
"optional": input.optional,
125+
"name": input_pin_info.name,
126+
"types": [str(t) for t in input_pin_info._type_names],
127+
"document": input_pin_info.document,
128+
"optional": input_pin_info.optional,
54129
}
55130
)
56131
for output_pin in spec.outputs:
@@ -103,7 +178,7 @@ def fetch_doc_info(server, operator_name):
103178
if category:
104179
op_friendly_name = category + ":" + op_friendly_name
105180

106-
license = properties.pop("license", "None")
181+
license_type = properties.pop("license", "None")
107182

108183
exposure = properties.pop("exposure", "private")
109184
scripting_info = {
@@ -112,7 +187,7 @@ def fetch_doc_info(server, operator_name):
112187
"scripting_name": scripting_name,
113188
"full_name": full_name,
114189
"internal_name": operator_name,
115-
"license": license,
190+
"license": license_type,
116191
"version": str(last_version), # Include last version in scripting_info
117192
"changelog": changelog_entries, # Include all changelog entries
118193
}
@@ -128,7 +203,22 @@ def fetch_doc_info(server, operator_name):
128203
}
129204

130205

131-
def get_plugin_operators(server, plugin_name):
206+
def get_plugin_operators(server: dpf.AnyServerType, plugin_name: str) -> list[str]:
207+
"""Get the list of operators for a given plugin.
208+
209+
Parameters
210+
----------
211+
server:
212+
DPF server to query for the list of operators.
213+
plugin_name:
214+
Name of the plugin of interest.
215+
216+
Returns
217+
-------
218+
operator_list:
219+
List of names of operators available on the server for the given plugin.
220+
221+
"""
132222
operators = available_operator_names(server)
133223
plugin_operators = []
134224
for operator_name in operators:
@@ -138,7 +228,23 @@ def get_plugin_operators(server, plugin_name):
138228
return plugin_operators
139229

140230

141-
def generate_operator_doc(server, operator_name, include_private, output_path):
231+
def generate_operator_doc(
232+
server: dpf.AnyServerType, operator_name: str, include_private: bool, output_path: Path
233+
):
234+
"""Write the Markdown documentation page for a given operator on a given DPF server.
235+
236+
Parameters
237+
----------
238+
server:
239+
DPF server of interest.
240+
operator_name:
241+
Name of the operator of interest.
242+
include_private:
243+
Whether to generate the documentation if the operator is private.
244+
output_path:
245+
Path to write the operator documentation at.
246+
247+
"""
142248
operator_info = fetch_doc_info(server, operator_name)
143249
scripting_name = operator_info["scripting_info"]["scripting_name"]
144250
category = operator_info["scripting_info"]["category"]
@@ -150,26 +256,32 @@ def generate_operator_doc(server, operator_name, include_private, output_path):
150256
file_name = file_name.replace("::", "_")
151257
if not include_private and operator_info["exposure"] == "private":
152258
return
153-
script_path = Path(__file__)
154-
root_dir = script_path.parent.parent
155-
template_dir = Path(root_dir) / "doc" / "source" / "operators_doc" / "operator-specifications"
156-
spec_folder = Path(output_path) / "operator-specifications"
259+
template_path = Path(__file__).parent / "operator_doc_template.md"
260+
spec_folder = output_path / "operator-specifications"
157261
category_dir = spec_folder / category
158262
spec_folder.mkdir(parents=True, exist_ok=True)
159263
if category is not None:
160264
category_dir.mkdir(parents=True, exist_ok=True) # Ensure all parent directories are created
161265
file_dir = category_dir
162266
else:
163-
file_dir = Path(output_path) / "operator-specifications"
164-
with Path.open(Path(template_dir) / "operator_doc_template.md", "r") as file:
165-
template = Template(file.read())
267+
file_dir = output_path / "operator-specifications"
268+
with Path.open(template_path, "r") as file:
269+
template = jinja2.Template(file.read())
166270

167271
output = template.render(operator_info)
168272
with Path.open(Path(file_dir) / f"{file_name}.md", "w") as file:
169273
file.write(output)
170274

171275

172-
def generate_toc_tree(docs_path):
276+
def generate_toc_tree(docs_path: Path):
277+
"""Write the global toc.yml file for the DPF documentation based on the operators found.
278+
279+
Parameters
280+
----------
281+
docs_path:
282+
Path to the root of the DPF documentation sources.
283+
284+
"""
173285
data = []
174286
specs_path = docs_path / "operator-specifications"
175287
for folder in specs_path.iterdir():
@@ -187,50 +299,76 @@ def generate_toc_tree(docs_path):
187299
data.append({"category": category, "operators": operators})
188300

189301
# Render the Jinja2 template
190-
script_path = Path(__file__)
191-
root_dir = script_path.parent.parent
192-
template_dir = Path(root_dir) / "doc" / "source" / "operators_doc" / "operator-specifications"
193-
template_path = template_dir / "toc_template.j2"
302+
template_path = Path(__file__).parent / "toc_template.j2"
194303
with Path.open(template_path, "r") as template_file:
195-
template = Template(template_file.read())
304+
template = jinja2.Template(template_file.read())
196305
output = template.render(data=data) # Pass 'data' as a named argument
197306

198307
# Write the rendered output to toc.yml at the operators_doc level
199-
# toc_path = docs_path / "toc.yml"
200308
with Path.open(docs_path / "toc.yml", "w") as file:
201309
file.write(output)
202310

203311

204-
def main():
312+
def generate_operators_doc(
313+
ansys_path: Path,
314+
output_path: Path,
315+
include_composites: bool = False,
316+
include_sound: bool = False,
317+
include_private: bool = False,
318+
desired_plugin: str = None,
319+
):
320+
"""Generate the Markdown source files for the DPF operator documentation.
321+
322+
This function generates a Markdown file for each operator found in a given DPF installation,
323+
categorized in folders per operator category, as well as a `toc.yml` file.
324+
These are used to generate the DPF html documentation website as seen on the Developer Portal.
325+
326+
Parameters
327+
----------
328+
ansys_path:
329+
Path to an Ansys/DPF installation.
330+
output_path:
331+
Path to write the output files at.
332+
include_composites:
333+
Whether to include operators of the Composites plugin.
334+
include_sound:
335+
Whether to include operators of the Sound plugin.
336+
include_private:
337+
Whether to include private operators.
338+
desired_plugin:
339+
Restrict documentation generation to the operators of this specific plugin.
340+
341+
"""
342+
server = initialize_server(ansys_path, include_composites, include_sound)
343+
if desired_plugin is None:
344+
operators = available_operator_names(server)
345+
else:
346+
operators = get_plugin_operators(server, desired_plugin)
347+
for operator_name in operators:
348+
generate_operator_doc(server, operator_name, include_private, output_path)
349+
generate_toc_tree(output_path)
350+
351+
352+
if __name__ == "__main__":
205353
parser = argparse.ArgumentParser(description="Fetch available operators")
206354
parser.add_argument("--plugin", help="Filter operators by plugin")
207355
parser.add_argument(
208356
"--ansys_path", default=None, help="Path to Ansys DPF Server installation directory"
209357
)
210-
parser.add_argument("--output_path", default=None, help="Path to output directory")
358+
parser.add_argument(
359+
"--output_path", default=None, help="Path to output directory", required=True
360+
)
211361
parser.add_argument("--include_private", action="store_true", help="Include private operators")
212362
parser.add_argument(
213363
"--include_composites", action="store_true", help="Include composites operators"
214364
)
215365
parser.add_argument("--include_sound", action="store_true", help="Include sound operators")
216366
args = parser.parse_args()
217-
desired_plugin = args.plugin
218-
output_path = (
219-
args.output_path
220-
if args.output_path
221-
else (Path(__file__).parent.parent / "doc" / "source" / "operators_doc")
222-
)
223367

224-
server = initialize_server(args.ansys_path, args.include_composites, args.include_sound)
225-
if desired_plugin is None:
226-
operators = available_operator_names(server)
227-
else:
228-
operators = get_plugin_operators(server, desired_plugin)
229-
for operator_name in operators:
230-
generate_operator_doc(server, operator_name, args.include_private, output_path)
231-
print(output_path)
232-
generate_toc_tree(Path(output_path))
233-
234-
235-
if __name__ == "__main__":
236-
main()
368+
generate_operators_doc(
369+
ansys_path=args.ansys_path,
370+
output_path=args.output_path,
371+
include_composites=args.include_composites,
372+
include_sound=args.include_sound,
373+
include_private=args.include_private,
374+
)
File renamed without changes.

0 commit comments

Comments
 (0)