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+
126import argparse
27+ from os import PathLike
228from pathlib import Path
329
4- from jinja2 import Template
5-
630from ansys .dpf import core as dpf
731from ansys .dpf .core .changelog import Changelog
832from ansys .dpf .core .core import load_library
933from 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+ )
0 commit comments