Skip to content

Commit 8b721c0

Browse files
authored
Feat: support Markdown in operator specs (#2115)
* Remove operators module files after successfully starting the server * Try converting MD to RST on-the-fly with pypandoc (way too slow) * Directly include the MD descriptions * Try without black formatting * Add MD but with indents * Properly expose Markdown descriptions in code generation * Fix mustache spacings * Fix mustache * Improve type hinting in operator.mustache * Use a dedicated indented string for pin docstrings * Remove superfluous newlines after the operator description in the docstring * Fix mustache for pin description in main operator docstring * Use black to reformat generated modules (does not scramble descriptions) * Code quality fix to mustache * Manage operator description separately * Print timings of generation * try pypandoc * Add Markdown2RstTranslator in ansys.dpf.core.operators.build and a test * Move Markdown2RstTranslator to ansys.dpf.core.operators.translator * Add pypandoc to requirements_test.txt * Add pypandoc_binary to requirements_test.txt
1 parent 1a588ed commit 8b721c0

File tree

6 files changed

+332
-68
lines changed

6 files changed

+332
-68
lines changed

.ci/code_generation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from ansys.dpf import core
99
from ansys.dpf.core.operators import build
1010

11+
core.set_default_server_context(core.AvailableServerContexts.premium)
12+
core.start_local_server(config=core.AvailableServerConfigs.LegacyGrpcServer)
13+
1114
local_dir = Path(__file__).parent
1215
TARGET_PATH = local_dir.parent / "src" / "ansys" / "dpf" / "core" / "operators"
1316
files = TARGET_PATH.glob("*")
@@ -26,7 +29,4 @@
2629
except:
2730
pass
2831

29-
core.set_default_server_context(core.AvailableServerContexts.premium)
30-
core.start_local_server(config=core.AvailableServerConfigs.LegacyGrpcServer)
31-
3232
build.build_operators()

requirements/requirements_test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ coverage==7.6.12
33
graphviz==0.20.1
44
imageio==2.36.1
55
imageio-ffmpeg==0.6.0
6+
pypandoc_binary==1.15
67
pytest==8.3.4
78
pytest-cov==6.0.0
89
pytest-order==1.3.0

src/ansys/dpf/core/operators/build.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
from datetime import datetime
55
from textwrap import wrap
6+
import time
67

78
import black
89
import chevron
@@ -11,18 +12,15 @@
1112
from ansys.dpf.core.dpf_operator import available_operator_names
1213
from ansys.dpf.core.outputs import _make_printable_type
1314
from ansys.dpf.core.mapping_types import map_types_to_python
15+
from ansys.dpf.core.operators.translator import Markdown2RstTranslator
1416

1517

16-
def build_docstring(specification):
18+
def build_docstring(specification_description):
1719
"""Used to generate class docstrings."""
1820
docstring = ""
19-
if specification.description:
20-
docstring += "\n".join(
21-
wrap(specification.description, subsequent_indent=" ")
22-
)
23-
docstring += "\n\n"
24-
docstring = docstring.rstrip()
25-
return docstring.replace('"', "'")
21+
if specification_description:
22+
docstring += specification_description.replace("\n", "\n ")
23+
return docstring
2624

2725

2826
def map_types(cpp_types):
@@ -83,6 +81,9 @@ def build_pin_data(pins, output=False):
8381
if multiple_types and output:
8482
printable_type_names = [_make_printable_type(name) for name in type_names]
8583

84+
document = specification.document
85+
document_pin_docstring = document.replace("\n", "\n ")
86+
8687
pin_data = {
8788
"id": id,
8889
"name": pin_name,
@@ -97,13 +98,8 @@ def build_pin_data(pins, output=False):
9798
"main_type": main_type,
9899
"built_in_main_type": main_type in built_in_types,
99100
"optional": specification.optional,
100-
"document": "\n".join(
101-
wrap(
102-
specification.document.capitalize().lstrip(' '),
103-
subsequent_indent=" ",
104-
width=45,
105-
)
106-
),
101+
"document": document,
102+
"document_pin_docstring": document_pin_docstring,
107103
"ellipsis": 0 if specification.ellipsis else -1,
108104
}
109105

@@ -125,7 +121,7 @@ def build_pin_data(pins, output=False):
125121

126122

127123
def build_operator(
128-
specification, operator_name, class_name, capital_class_name, category
124+
specification, operator_name, class_name, capital_class_name, category, specification_description
129125
):
130126

131127
input_pins = []
@@ -137,11 +133,7 @@ def build_operator(
137133
output_pins = build_pin_data(specification.outputs, output=True)
138134
multiple_output_types = any(pin["multiple_types"] for pin in output_pins)
139135

140-
docstring = build_docstring(specification)
141-
142-
specification_description = "\n".join(
143-
wrap(specification.description, subsequent_indent=" ")
144-
)
136+
docstring = build_docstring(specification_description)
145137

146138
date_and_time = datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
147139

@@ -173,6 +165,7 @@ def build_operator(
173165

174166
def build_operators():
175167
print(f"Generating operators for server {dpf.SERVER.version}")
168+
time_0 = time.time()
176169

177170
this_path = os.path.dirname(os.path.abspath(__file__))
178171

@@ -192,6 +185,9 @@ def build_operators():
192185
"invert", "invert_fc",
193186
]
194187
categories = set()
188+
189+
translator = Markdown2RstTranslator()
190+
195191
for operator_name in available_operators:
196192
if succeeded == done + 100:
197193
done += 100
@@ -226,6 +222,9 @@ def build_operators():
226222
# Get python class name from scripting name
227223
capital_class_name = common._snake_to_camel_case(scripting_name)
228224

225+
# Convert Markdown descriptions to RST
226+
specification_description = translator.convert(specification.description)
227+
229228
# Write to operator file
230229
operator_file = os.path.join(category_path, scripting_name + ".py")
231230
with open(operator_file, "wb") as f:
@@ -236,6 +235,7 @@ def build_operators():
236235
scripting_name,
237236
capital_class_name,
238237
category,
238+
specification_description,
239239
)
240240
exec(operator_str, globals())
241241
f.write(operator_str.encode())
@@ -269,6 +269,7 @@ def build_operators():
269269

270270
if succeeded == len(available_operators) - hidden:
271271
print("Success")
272+
print(f"Took {time.time() - time_0}")
272273
exit(0)
273274
else:
274275
print("Terminated with errors")

src/ansys/dpf/core/operators/operator.mustache

Lines changed: 61 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
Autogenerated DPF operator classes.
55
"""
66

7+
from __future__ import annotations
8+
79
from warnings import warn
810
from ansys.dpf.core.dpf_operator import Operator
911
from ansys.dpf.core.inputs import Input, _Inputs
@@ -17,36 +19,39 @@ from ansys.dpf.core.outputs import _Outputs
1719
from ansys.dpf.core.outputs import _modify_output_spec_with_one_type
1820
{{/multiple_output_types}}
1921
from ansys.dpf.core.operators.specification import PinSpecification, Specification
22+
from ansys.dpf.core.config import Config
23+
from ansys.dpf.core.server_types import AnyServerType
24+
2025

2126
class {{class_name}}(Operator):
22-
"""{{{docstring}}}
27+
r"""{{{docstring}}}
2328

2429
Parameters
2530
----------
2631
{{#input_pins}}
2732
{{#optional}}
28-
{{name}} :{{#types_for_docstring}} {{types_for_docstring}},{{/types_for_docstring}} optional
33+
{{name}}:{{#types_for_docstring}} {{types_for_docstring}},{{/types_for_docstring}} optional
2934
{{/optional}}
3035
{{^optional}}
31-
{{name}} :{{#types_for_docstring}} {{types_for_docstring}}{{/types_for_docstring}}
36+
{{name}}:{{#types_for_docstring}} {{types_for_docstring}}{{/types_for_docstring}}
3237
{{/optional}}
33-
{{#document}}
34-
{{{document}}}
35-
{{/document}}
38+
{{#document_pin_docstring}}
39+
{{{document_pin_docstring}}}
40+
{{/document_pin_docstring}}
3641
{{/input_pins}}
3742

3843
Returns
3944
-------
4045
{{#output_pins}}
4146
{{#optional}}
42-
{{name}} :{{#types_for_docstring}} {{types_for_docstring}},{{/types_for_docstring}} optional
47+
{{name}}:{{#types_for_docstring}} {{types_for_docstring}},{{/types_for_docstring}} optional
4348
{{/optional}}
4449
{{^optional}}
45-
{{name}} :{{#types_for_docstring}} {{types_for_docstring}}{{/types_for_docstring}}
50+
{{name}}:{{#types_for_docstring}} {{types_for_docstring}}{{/types_for_docstring}}
4651
{{/optional}}
47-
{{#document}}
48-
{{{document}}}
49-
{{/document}}
52+
{{#document_pin_docstring}}
53+
{{{document_pin_docstring}}}
54+
{{/document_pin_docstring}}
5055
{{/output_pins}}
5156

5257
Examples
@@ -87,8 +92,8 @@ class {{class_name}}(Operator):
8792
{{/input_pins}}
8893

8994
@staticmethod
90-
def _spec():
91-
description = """{{specification_description}}"""
95+
def _spec() -> Specification:
96+
description = r"""{{{specification_description}}}"""
9297
spec = Specification(
9398
description=description,
9499
map_input_pin_spec={
@@ -102,7 +107,7 @@ class {{class_name}}(Operator):
102107
type_names=["any"],
103108
{{/has_types}}
104109
optional={{optional}},
105-
document="""{{{document}}}""",
110+
document=r"""{{{document}}}""",
106111
{{#has_derived_class}}
107112
name_derived_class=["{{{derived_type_name}}}"],
108113
{{/has_derived_class}}
@@ -117,7 +122,7 @@ class {{class_name}}(Operator):
117122
type_names={{{types}}},
118123
{{/has_types}}
119124
optional={{optional}},
120-
document="""{{{document}}}""",
125+
document=r"""{{{document}}}""",
121126
{{#has_derived_class}}
122127
name_derived_class=["{{{derived_type_name}}}"],
123128
{{/has_derived_class}}
@@ -128,7 +133,7 @@ class {{class_name}}(Operator):
128133
return spec
129134

130135
@staticmethod
131-
def default_config(server=None):
136+
def default_config(server: AnyServerType = None) -> Config:
132137
"""Returns the default config of the operator.
133138

134139
This config can then be changed to the user needs and be used to
@@ -137,32 +142,40 @@ class {{class_name}}(Operator):
137142

138143
Parameters
139144
----------
140-
server : server.DPFServer, optional
145+
server:
141146
Server with channel connected to the remote or local instance. When
142147
``None``, attempts to use the global server.
148+
149+
Returns
150+
-------
151+
config:
152+
A new Config instance equivalent to the default config for this operator.
143153
"""
144154
return Operator.default_config(name="{{operator_name}}", server=server)
145155

146156
@property
147-
def inputs(self):
157+
def inputs(self) -> Inputs{{capital_class_name}}:
148158
"""Enables to connect inputs to the operator
149159

150160
Returns
151161
--------
152-
inputs : Inputs{{capital_class_name}}
162+
inputs:
163+
An instance of Inputs{{capital_class_name}}.
153164
"""
154165
return super().inputs
155166

156167
@property
157-
def outputs(self):
168+
def outputs(self) -> Outputs{{capital_class_name}}:
158169
"""Enables to get outputs of the operator by evaluating it
159170

160171
Returns
161172
--------
162-
outputs : Outputs{{capital_class_name}}
173+
outputs:
174+
An instance of Outputs{{capital_class_name}}.
163175
"""
164176
return super().outputs
165177

178+
166179
class Inputs{{capital_class_name}}(_Inputs):
167180
"""Intermediate class used to connect user inputs to
168181
{{class_name}} operator.
@@ -183,19 +196,20 @@ class Inputs{{capital_class_name}}(_Inputs):
183196
self._{{name}} = Input({{class_name}}._spec().input_pin({{id}}), {{id}}, op, {{ellipsis}})
184197
self._inputs.append(self._{{name}})
185198
{{/input_pins}}
186-
187199
{{#input_pins}}
200+
188201
@property
189-
def {{name}}(self):
190-
"""Allows to connect {{name}} input to the operator.
191-
{{#document}}
202+
def {{name}}(self) -> Input:
203+
r"""Allows to connect {{name}} input to the operator.
204+
{{#document_pin_docstring}}
192205

193-
{{{document}}}
194-
{{/document}}
206+
{{{document_pin_docstring}}}
207+
{{/document_pin_docstring}}
195208

196-
Parameters
197-
----------
198-
my_{{name}} :{{#types_for_docstring}} {{types_for_docstring}}{{/types_for_docstring}}
209+
Returns
210+
-------
211+
input:
212+
An Input instance for this pin.
199213

200214
Examples
201215
--------
@@ -206,8 +220,9 @@ class Inputs{{capital_class_name}}(_Inputs):
206220
>>> op.inputs.{{name}}(my_{{name}})
207221
"""
208222
return self._{{name}}
209-
210223
{{/input_pins}}
224+
225+
211226
class Outputs{{capital_class_name}}(_Outputs):
212227
"""Intermediate class used to get outputs from
213228
{{class_name}} operator.
@@ -236,27 +251,29 @@ class Outputs{{capital_class_name}}(_Outputs):
236251
self._outputs.append(self._{{name}})
237252
{{/multiple_types}}
238253
{{/output_pins}}
239-
{{#output_pins}}{{^multiple_types}}
240254

255+
{{#output_pins}}
256+
{{^multiple_types}}
241257
@property
242-
def {{name}}(self):
243-
"""Allows to get {{name}} output of the operator
258+
def {{name}}(self) -> Output:
259+
r"""Allows to get {{name}} output of the operator
260+
{{#document_pin_docstring}}
261+
262+
{{{document_pin_docstring}}}
263+
{{/document_pin_docstring}}
244264

245265
Returns
246-
----------
247-
{{#derived_type_name}}
248-
my_{{name}} : {{derived_type_name}}
249-
{{/derived_type_name}}
250-
{{^derived_type_name}}
251-
my_{{name}} :{{#types_for_docstring}} {{types_for_docstring}}{{/types_for_docstring}}
252-
{{/derived_type_name}}
266+
-------
267+
output:
268+
An Output instance for this pin.
253269

254270
Examples
255271
--------
256272
>>> from ansys.dpf import core as dpf
257273
>>> op = dpf.operators.{{category}}.{{class_name}}()
258-
>>> # Connect inputs : op.inputs. ...
274+
>>> # Get the output from op.outputs. ...
259275
>>> result_{{name}} = op.outputs.{{name}}()
260-
""" # noqa: E501
276+
"""
261277
return self._{{name}}
262-
{{/multiple_types}}{{/output_pins}}
278+
{{/multiple_types}}
279+
{{/output_pins}}

0 commit comments

Comments
 (0)